From 03d5a133eb1e6f8b3825a0dd1bbc13ccdeb1289b Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Sat, 24 May 2025 15:32:27 +0200 Subject: [PATCH 01/47] Close sqlite pool before erroring --- .../src/backend/fs_backend/error.rs | 5 +- .../src/backend/fs_backend/manager.rs | 10 +- .../client/base_client/non_wasm_helpers.rs | 100 +++++++++++++----- common/client-core/surb-storage/Cargo.toml | 8 +- .../src/backend/fs_backend/error.rs | 5 +- .../src/backend/fs_backend/manager.rs | 8 +- .../src/backend/fs_backend/mod.rs | 81 ++++++++------ .../src/persistent_storage/mod.rs | 1 + common/gateway-stats-storage/src/lib.rs | 1 + common/gateway-storage/src/lib.rs | 1 + 10 files changed, 147 insertions(+), 73 deletions(-) diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs index bdcaa0fdb5..59b7bef89d 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::BadGateway; -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -19,7 +18,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -32,7 +30,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs index 101a6a8710..0b7aafdb28 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -47,12 +47,14 @@ impl StorageManager { StorageError::DatabaseConnectionError { source } })?; - sqlx::migrate!("./fs_gateways_migrations") + if let Err(err) = sqlx::migrate!("./fs_gateways_migrations") .run(&connection_pool) .await - .inspect_err(|err| { - error!("Failed to initialize SQLx database: {err}"); - })?; + { + error!("Failed to initialize SQLx database: {err}"); + connection_pool.close().await; + return Err(err)?; + } debug!("Database migration finished!"); Ok(StorageManager { connection_pool }) diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index e7ef621c9d..4f3a2c0c77 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -1,20 +1,22 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, ReplyStorageBackend, +use crate::{ + client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend}, + config, + config::Config, + error::ClientCoreError, }; -use crate::config; -use crate::config::Config; -use crate::error::ClientCoreError; +#[cfg(windows)] +use log::debug; use log::{error, info, trace}; use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; -use nym_validator_client::nyxd; -use nym_validator_client::QueryHttpRpcNyxdClient; -use std::path::Path; -use std::{fs, io}; +use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; +#[cfg(windows)] +use std::time::Duration; +use std::{io, path::Path}; use time::OffsetDateTime; use url::Url; @@ -22,11 +24,14 @@ async fn setup_fresh_backend>( db_path: P, surb_config: &config::ReplySurbs, ) -> Result { - info!("creating fresh surb database"); + info!( + "creating fresh surb database: {}", + db_path.as_ref().display() + ); let mut storage_backend = match fs_backend::Backend::init(db_path).await { Ok(backend) => backend, Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); + error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"); return Err(ClientCoreError::SurbStorageError { source: Box::new(err), }); @@ -40,14 +45,15 @@ async fn setup_fresh_backend>( surb_config.minimum_reply_surb_storage_threshold, surb_config.maximum_reply_surb_storage_threshold, ); - storage_backend - .init_fresh(&mem_store) - .await - .map_err(|err| ClientCoreError::SurbStorageError { - source: Box::new(err), - })?; - - Ok(storage_backend) + match storage_backend.init_fresh(&mem_store).await { + Ok(()) => Ok(storage_backend), + Err(err) => { + storage_backend.shutdown().await; + Err(ClientCoreError::SurbStorageError { + source: Box::new(err), + }) + } + } } // fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend { @@ -58,12 +64,11 @@ async fn setup_fresh_backend>( // ) // } -fn archive_corrupted_database>(db_path: P) -> io::Result<()> { +async fn archive_corrupted_database>(db_path: P) -> io::Result<()> { let db_path = db_path.as_ref(); debug_assert!(db_path.exists()); let now = OffsetDateTime::now_utc().unix_timestamp(); - let suffix = format!("_{now}.corrupted"); let new_extension = @@ -72,11 +77,51 @@ fn archive_corrupted_database>(db_path: P) -> io::Result<()> { } else { suffix }; + let renamed = db_path.with_extension(new_extension); - let mut renamed = db_path.to_owned(); - renamed.set_extension(new_extension); + rename_db_file(&db_path, &renamed).await.inspect_err(|_| { + tracing::error!( + "Failed to rename corrupt database file: {} to {}", + db_path.display(), + renamed.display() + ); + }) +} - fs::rename(db_path, renamed) +#[cfg(not(windows))] +async fn rename_db_file(db_path: impl AsRef, renamed: impl AsRef) -> io::Result<()> { + tokio::fs::rename(db_path, &renamed).await +} + +#[cfg(windows)] +async fn rename_db_file(db_path: impl AsRef, renamed: impl AsRef) -> io::Result<()> { + // Due to bug in sqlx (https://github.com/launchbadge/sqlx/issues/3217), + // the sqlite file can be still in use after closing sqlite connection pool + // Poll for a bit until the db file is released. + + // Max number of retries + const MAX_RETRY_ATTEMPTS: u32 = 10; + // Delay between retries + const WAIT_DELAY: Duration = Duration::from_millis(100); + // Error code returned when file is still in use. + const FILE_IN_USE_ERR: i32 = 32; + + let mut retry_attempt = 0; + while let Err(e) = tokio::fs::rename(db_path.as_ref(), renamed.as_ref()).await { + retry_attempt += 1; + + if e.raw_os_error() == Some(FILE_IN_USE_ERR) && retry_attempt < MAX_RETRY_ATTEMPTS { + debug!( + "File {} is still open. Sleep and retry", + db_path.as_ref().display() + ); + tokio::time::sleep(WAIT_DELAY).await; + } else { + return Err(e); + } + } + + Ok(()) } pub async fn setup_fs_reply_surb_backend>( @@ -87,13 +132,12 @@ pub async fn setup_fs_reply_surb_backend>( // the existing one let db_path = db_path.as_ref(); if db_path.exists() { - info!("loading existing surb database"); + info!("Loading existing surb database: {}", db_path.display()); match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await { Ok(backend) => Ok(backend), Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); - - archive_corrupted_database(db_path)?; + error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); + archive_corrupted_database(db_path).await?; setup_fresh_backend(db_path, surb_config).await } } diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 7a902bef94..49dfbe4b8c 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -12,6 +12,7 @@ dashmap.workspace = true log.workspace = true thiserror.workspace = true time.workspace = true +tokio = { workspace = true, features = ["fs"] } nym-crypto = { path = "../../crypto", optional = true, default-features = false } nym-sphinx = { path = "../../nymsphinx" } @@ -25,7 +26,12 @@ optional = true [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } [features] fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"] diff --git a/common/client-core/surb-storage/src/backend/fs_backend/error.rs b/common/client-core/surb-storage/src/backend/fs_backend/error.rs index 02742758ae..e1eb9036da 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/error.rs @@ -1,8 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -30,7 +29,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -43,7 +41,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 02316ddb7f..3daa4744a4 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -17,7 +17,7 @@ use std::path::Path; #[derive(Debug, Clone)] pub struct StorageManager { - pub connection_pool: sqlx::SqlitePool, + connection_pool: sqlx::SqlitePool, } // all SQL goes here @@ -54,6 +54,7 @@ impl StorageManager { .await { error!("Failed to initialize SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } @@ -61,6 +62,11 @@ impl StorageManager { Ok(StorageManager { connection_pool }) } + /// Close connection pool waiting for all connections to be closed. + pub async fn close_pool(&self) { + self.connection_pool.close().await; + } + #[allow(dead_code)] pub async fn status_table_exists(&self) -> Result { sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") diff --git a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs index 4467e17538..1f7e9bc198 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs @@ -1,18 +1,21 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::backend::fs_backend::manager::StorageManager; -use crate::backend::fs_backend::models::{ - ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, -}; -use crate::surb_storage::ReceivedReplySurbs; use crate::{ - CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, + backend::fs_backend::{ + manager::StorageManager, + models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, + StoredSurbSender, + }, + }, + surb_storage::ReceivedReplySurbs, + CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, + UsedSenderTags, }; use async_trait::async_trait; use log::{debug, error, info, warn}; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use std::fs; use std::path::{Path, PathBuf}; use time::OffsetDateTime; @@ -41,15 +44,17 @@ impl Backend { } let manager = StorageManager::init(database_path, true).await?; - manager.create_status_table().await?; - - let backend = Backend { - temporary_old_path: None, - database_path: owned_path, - manager, - }; - - Ok(backend) + match manager.create_status_table().await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(err) => { + manager.close_pool().await; + Err(err.into()) + } + } } pub async fn try_load>( @@ -64,7 +69,28 @@ impl Backend { } let manager = StorageManager::init(database_path, false).await?; + match Self::try_load_inner(&manager, fresh_sender_tags).await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(e) => { + manager.close_pool().await; + Err(e) + } + } + } + /// Gracefully close sqlite connection pool and drop backend. + pub async fn shutdown(self) { + self.manager.close_pool().await + } + + async fn try_load_inner( + manager: &StorageManager, + fresh_sender_tags: bool, + ) -> Result<(), StorageError> { // the database flush wasn't fully finished and thus the data is in inconsistent state // (we don't really know what's properly saved or what's not) if manager.get_flush_status().await? { @@ -126,20 +152,11 @@ impl Backend { manager.delete_all_tags().await?; } - Ok(Backend { - temporary_old_path: None, - database_path: owned_path, - // manager: StorageManagerState::Storage(manager), - manager, - }) - } - - async fn close_pool(&mut self) { - self.manager.connection_pool.close().await; + Ok(()) } async fn rotate(&mut self) -> Result<(), StorageError> { - self.close_pool().await; + self.manager.close_pool().await; let new_extension = if let Some(existing_extension) = self.database_path.extension().and_then(|ext| ext.to_str()) @@ -152,7 +169,8 @@ impl Backend { let mut temp_old = self.database_path.clone(); temp_old.set_extension(new_extension); - fs::rename(&self.database_path, &temp_old) + tokio::fs::rename(&self.database_path, &temp_old) + .await .map_err(|err| StorageError::DatabaseRenameError { source: err })?; self.manager = StorageManager::init(&self.database_path, true).await?; self.manager.create_status_table().await?; @@ -161,9 +179,10 @@ impl Backend { Ok(()) } - fn remove_old(&mut self) -> Result<(), StorageError> { + async fn remove_old(&mut self) -> Result<(), StorageError> { if let Some(old_path) = self.temporary_old_path.take() { - fs::remove_file(old_path) + tokio::fs::remove_file(old_path) + .await .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) } else { warn!("the old database file doesn't seem to exist!"); @@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend { self.dump_reply_surb_storage_metadata(surbs_ref).await?; self.dump_reply_surbs(surbs_ref).await?; - self.remove_old()?; + self.remove_old().await?; self.end_storage_flush().await } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 565ab94513..bccbccf4ab 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -76,6 +76,7 @@ impl PersistentStorage { if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 8ad34bbca4..10aa61062d 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -59,6 +59,7 @@ impl PersistentStatsStorage { if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 2d05d43fe7..1d91576ccb 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -108,6 +108,7 @@ impl GatewayStorage { if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } From a215b3d0bf409e13b0483bbb0fc26015a7dae783 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Wed, 28 May 2025 21:04:40 +0200 Subject: [PATCH 02/47] Open file watch --- Cargo.lock | 19 ++++ Cargo.toml | 3 +- sqlx-pool-guard/Cargo.toml | 16 +++ sqlx-pool-guard/src/lib.rs | 201 +++++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 sqlx-pool-guard/Cargo.toml create mode 100644 sqlx-pool-guard/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4cad468c66..13a80c207b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7954,6 +7954,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc_pidinfo" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1724fdb3f7349e4c58806dfb29dbc3b5e2f8b0997bb5117acf8db6e555867a91" +dependencies = [ + "libc", +] + [[package]] name = "prometheus" version = "0.14.0" @@ -9525,6 +9534,16 @@ dependencies = [ "whoami", ] +[[package]] +name = "sqlx-pool-guard" +version = "0.1.0" +dependencies = [ + "log", + "proc_pidinfo", + "sqlx", + "tokio", +] + [[package]] name = "sqlx-postgres" version = "0.8.6" diff --git a/Cargo.toml b/Cargo.toml index c2f5b97078..e15ac95c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ members = [ "service-providers/authenticator", "service-providers/common", "service-providers/ip-packet-router", - "service-providers/network-requester", + "service-providers/network-requester", "sqlx-pool-guard", "tools/echo-server", "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", @@ -286,6 +286,7 @@ petgraph = "0.6.5" pin-project = "1.1" pin-project-lite = "0.2.16" publicsuffix = "2.3.0" +proc_pidinfo = "0.1" quote = "1" rand = "0.8.5" rand_chacha = "0.3" diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml new file mode 100644 index 0000000000..ee60690b4d --- /dev/null +++ b/sqlx-pool-guard/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sqlx-pool-guard" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[lints] +workspace = true + +[dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time", "fs"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +log.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +proc_pidinfo.workspace = true diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs new file mode 100644 index 0000000000..05a933fe5f --- /dev/null +++ b/sqlx-pool-guard/src/lib.rs @@ -0,0 +1,201 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::Duration, +}; + +#[cfg(target_os = "macos")] +use proc_pidinfo::{ + ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, +}; + +const SQL_CLOSE_MAX_ATTEMPTS: u8 = 10; +const SQL_CLOSE_RETRY_DELAY: Duration = Duration::from_millis(100); + +pub struct SqlitePoolGuard { + database_path: PathBuf, + connection_pool: sqlx::SqlitePool, +} + +impl Deref for SqlitePoolGuard { + type Target = sqlx::SqlitePool; + fn deref(&self) -> &Self::Target { + &self.connection_pool + } +} + +impl DerefMut for SqlitePoolGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.connection_pool + } +} + +impl SqlitePoolGuard { + pub fn new(database_path: PathBuf, connection_pool: sqlx::SqlitePool) -> Self { + Self { + database_path, + connection_pool, + } + } + + /// Close udnerlying sqlite pool + pub async fn close_pool(&self) { + _ = self.close_pool_inner(); + } + + async fn close_pool_inner(&self) -> std::io::Result<()> { + self.connection_pool.close().await; + + #[cfg(target_os = "macos")] + if let Err(e) = self.wait_io_close().await { + log::error!("Failed to wait for file to close: {e}"); + } + + Ok(()) + } + + /// Returns all database files, including shm and wal files. + fn all_database_files(&self) -> Vec { + let mut database_files = vec![]; + let canonical_path = self + .database_path + .canonicalize() + .inspect_err(|e| { + log::error!( + "Failed to canonicalize path: {}. Cause: {e}", + self.database_path.display() + ); + }) + .unwrap_or(self.database_path.clone()); + + if let Some(ext) = canonical_path.extension() { + for added_ext in ["-shm", "-wal"] { + let mut new_ext = ext.to_owned(); + new_ext.push(added_ext); + database_files.push(canonical_path.with_extension(new_ext)); + } + } + database_files.push(canonical_path); + database_files + } + + /// Wait for I/O close to the database files + async fn wait_io_close(&self) -> std::io::Result<()> { + let database_files = self.all_database_files(); + let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); + + for _ in 0..SQL_CLOSE_MAX_ATTEMPTS { + match Self::check_io_close(&paths) + .await + .inspect_err(|e| log::error!("check_io_close() failure: {e}")) + { + Ok(true) | Err(_) => tokio::time::sleep(SQL_CLOSE_RETRY_DELAY).await, + Ok(false) => return Ok(()), + } + } + + Err(io::Error::new( + io::ErrorKind::TimedOut, + "timed out waiting for sqlite files to be closed", + )) + } + + /// Check if no more open file descriptors exist for the given files. + /// + /// On macOS this is done using `proc_pidinfo` (`sys/proc_info.h`) + /// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ + #[cfg(target_os = "macos")] + async fn check_io_close(file_paths: &[&Path]) -> io::Result { + let fd_list = proc_pidinfo_list_self::()?; + + for fd in fd_list + .iter() + .filter(|s| s.fd_type() == Ok(ProcFDType::VNODE)) + { + let Some(vnode) = proc_pidfdinfo_self::(fd.proc_fd) + .inspect_err(|e| { + log::warn!("proc_pidfdinfo_self::() failure: {e}"); + }) + .ok() + .flatten() + else { + continue; + }; + + if let Ok(true) = vnode + .path() + .map(|vnode_path| file_paths.contains(&vnode_path)) + .inspect_err(|e| { + log::warn!("vnode.path() failure: {e:?}"); + }) + { + return Ok(true); + } + } + + Ok(false) + } + + /// Check if no more open file descriptors exist for the given files. + #[cfg(target_os = "linux")] + async fn check_io_close(file_paths: &[&Path]) -> io::Result { + let mut dir = tokio::fs::read_dir("/proc/self/fd/").await?; + + while let Ok(Some(entry)) = dir.next_entry().await { + // Looking for DT_LNK + if entry + .file_type() + .await + .inspect_err(|e| log::warn!("entry.file_type() failure: {e}")) + .is_ok_and(|entry_type| entry_type.is_symlink()) + { + // atoi(d->d_name) to obtain open fd? + let path = PathBuf::from(entry.file_name()); + if file_paths.contains(&path.as_ref()) { + return Ok(true); + } + } + } + + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use sqlx::{ + ConnectOptions, + sqlite::{SqliteAutoVacuum, SqliteSynchronous}, + }; + + use super::*; + + #[tokio::test] + async fn test_wait_close() { + let database_path = PathBuf::from("/tmp/storage.sqlite"); + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .auto_vacuum(SqliteAutoVacuum::Incremental) + .filename(database_path.clone()) + .create_if_missing(true) + .disable_statement_logging(); + let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap(); + + let guard = SqlitePoolGuard::new(database_path, connection_pool); + + assert!( + guard + .wait_io_close() + .await + .err() + .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut) + ); + + assert!(guard.close_pool_inner().await.is_ok()); + } +} From 548b8717b2b2ada5679389e3a8078c44e6c80f43 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 29 May 2025 11:13:57 +0200 Subject: [PATCH 03/47] Update Linux impl --- Cargo.lock | 1 + sqlx-pool-guard/Cargo.toml | 3 +++ sqlx-pool-guard/src/lib.rs | 30 ++++++++++++++++++------------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13a80c207b..3a13a133f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9541,6 +9541,7 @@ dependencies = [ "log", "proc_pidinfo", "sqlx", + "tempfile", "tokio", ] diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index ee60690b4d..e0b4452d3c 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -14,3 +14,6 @@ log.workspace = true [target.'cfg(target_os = "macos")'.dependencies] proc_pidinfo.workspace = true + +[dev-dependencies] +tempfile = { workspace = true } \ No newline at end of file diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index 05a933fe5f..eb7f0ea568 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -50,7 +50,6 @@ impl SqlitePoolGuard { async fn close_pool_inner(&self) -> std::io::Result<()> { self.connection_pool.close().await; - #[cfg(target_os = "macos")] if let Err(e) = self.wait_io_close().await { log::error!("Failed to wait for file to close: {e}"); } @@ -84,6 +83,12 @@ impl SqlitePoolGuard { } /// Wait for I/O close to the database files + /// + /// - macOS: uses `proc_pidinfo` (`sys/proc_info.h`) + /// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ + /// + /// - Linux, Android: uses `/proc/self/fd/` to list open file descriptors + /// See: https://stackoverflow.com/a/59797198/351305 async fn wait_io_close(&self) -> std::io::Result<()> { let database_files = self.all_database_files(); let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); @@ -105,9 +110,6 @@ impl SqlitePoolGuard { } /// Check if no more open file descriptors exist for the given files. - /// - /// On macOS this is done using `proc_pidinfo` (`sys/proc_info.h`) - /// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ #[cfg(target_os = "macos")] async fn check_io_close(file_paths: &[&Path]) -> io::Result { let fd_list = proc_pidinfo_list_self::()?; @@ -141,22 +143,26 @@ impl SqlitePoolGuard { } /// Check if no more open file descriptors exist for the given files. - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] async fn check_io_close(file_paths: &[&Path]) -> io::Result { let mut dir = tokio::fs::read_dir("/proc/self/fd/").await?; while let Ok(Some(entry)) = dir.next_entry().await { - // Looking for DT_LNK if entry .file_type() .await .inspect_err(|e| log::warn!("entry.file_type() failure: {e}")) .is_ok_and(|entry_type| entry_type.is_symlink()) { - // atoi(d->d_name) to obtain open fd? - let path = PathBuf::from(entry.file_name()); - if file_paths.contains(&path.as_ref()) { - return Ok(true); + match tokio::fs::read_link(entry.path()).await { + Ok(resolved_path) => { + if file_paths.contains(&resolved_path.as_ref()) { + return Ok(true); + } + } + Err(e) => { + log::error!("Failed to read symlink: {e}"); + } } } } @@ -176,7 +182,8 @@ mod tests { #[tokio::test] async fn test_wait_close() { - let database_path = PathBuf::from("/tmp/storage.sqlite"); + let temp_dir = tempfile::tempdir().unwrap(); + let database_path = temp_dir.path().join("storage.sqlite"); let opts = sqlx::sqlite::SqliteConnectOptions::new() .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) @@ -187,7 +194,6 @@ mod tests { let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap(); let guard = SqlitePoolGuard::new(database_path, connection_pool); - assert!( guard .wait_io_close() From 4eedbb235af4576c769e4f9ecfbf494d0bf93d33 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 29 May 2025 11:30:34 +0200 Subject: [PATCH 04/47] Add Windows implementation --- sqlx-pool-guard/src/lib.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index eb7f0ea568..b3e0f672bc 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -89,6 +89,8 @@ impl SqlitePoolGuard { /// /// - Linux, Android: uses `/proc/self/fd/` to list open file descriptors /// See: https://stackoverflow.com/a/59797198/351305 + /// + /// - Windows: attempts to open files to detect whether they are still open. async fn wait_io_close(&self) -> std::io::Result<()> { let database_files = self.all_database_files(); let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); @@ -98,8 +100,8 @@ impl SqlitePoolGuard { .await .inspect_err(|e| log::error!("check_io_close() failure: {e}")) { - Ok(true) | Err(_) => tokio::time::sleep(SQL_CLOSE_RETRY_DELAY).await, - Ok(false) => return Ok(()), + Ok(false) | Err(_) => tokio::time::sleep(SQL_CLOSE_RETRY_DELAY).await, + Ok(true) => return Ok(()), } } @@ -135,11 +137,11 @@ impl SqlitePoolGuard { log::warn!("vnode.path() failure: {e:?}"); }) { - return Ok(true); + return Ok(false); } } - Ok(false) + Ok(true) } /// Check if no more open file descriptors exist for the given files. @@ -157,7 +159,7 @@ impl SqlitePoolGuard { match tokio::fs::read_link(entry.path()).await { Ok(resolved_path) => { if file_paths.contains(&resolved_path.as_ref()) { - return Ok(true); + return Ok(false); } } Err(e) => { @@ -167,7 +169,29 @@ impl SqlitePoolGuard { } } - Ok(false) + Ok(true) + } + + #[cfg(windows)] + async fn check_io_close(file_paths: &[&Path]) -> io::Result { + // Error code returned when file is still in use. + const FILE_IN_USE_ERR: i32 = 32; + + for file_path in file_paths { + if let Err(e) = tokio::fs::OpenOptions::new() + .read(true) + .open(file_path) + .await + { + if e.raw_os_error() == Some(FILE_IN_USE_ERR) { + return Ok(false); + } else if e.kind() != io::ErrorKind::NotFound { + log::error!("Failed to open file: {}", file_path.display()); + } + } + } + + Ok(true) } } From c225511f951bf3f6480e16efa3d03fb50737d58b Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 29 May 2025 18:14:55 +0200 Subject: [PATCH 05/47] Add Windows impl --- Cargo.lock | 109 +++++++++++++++++++-- sqlx-pool-guard/Cargo.toml | 19 +++- sqlx-pool-guard/src/lib.rs | 141 +++++++------------------- sqlx-pool-guard/src/linux.rs | 34 +++++++ sqlx-pool-guard/src/macos.rs | 41 ++++++++ sqlx-pool-guard/src/windows.rs | 174 +++++++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+), 115 deletions(-) create mode 100644 sqlx-pool-guard/src/linux.rs create mode 100644 sqlx-pool-guard/src/macos.rs create mode 100644 sqlx-pool-guard/src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 3a13a133f4..01820eb102 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9543,6 +9543,7 @@ dependencies = [ "sqlx", "tempfile", "tokio", + "windows 0.61.1", ] [[package]] @@ -11559,6 +11560,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -11593,6 +11616,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -11615,6 +11662,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -11638,10 +11696,31 @@ dependencies = [ ] [[package]] -name = "windows-link" -version = "0.1.0" +name = "windows-interface" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] [[package]] name = "windows-registry" @@ -11649,7 +11728,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.1", + "windows-result 0.3.4", "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -11674,9 +11753,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -11700,6 +11779,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -11798,6 +11886,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index e0b4452d3c..a77ee80024 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -8,12 +8,27 @@ license.workspace = true workspace = true [dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time", "fs"] } +tokio = { workspace = true, features = [ + "rt-multi-thread", + "macros", + "time", + "fs", +] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } log.workspace = true [target.'cfg(target_os = "macos")'.dependencies] proc_pidinfo.workspace = true +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32", + "Win32_System", + "Win32_System_Memory", + "Win32_System_Threading", + "Win32_Storage_FileSystem", + "Wdk_System_SystemInformation", +] } + [dev-dependencies] -tempfile = { workspace = true } \ No newline at end of file +tempfile = { workspace = true } diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index b3e0f672bc..8b120ee261 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -8,13 +8,23 @@ use std::{ time::Duration, }; -#[cfg(target_os = "macos")] -use proc_pidinfo::{ - ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, -}; +#[cfg(windows)] +#[path = "windows.rs"] +mod imp; -const SQL_CLOSE_MAX_ATTEMPTS: u8 = 10; -const SQL_CLOSE_RETRY_DELAY: Duration = Duration::from_millis(100); +#[cfg(target_os = "macos")] +#[path = "macos.rs"] +mod imp; + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[path = "linux.rs"] +mod imp; + +/// Max number of retry attempts +const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 10; + +/// Delay between file checks +const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100); pub struct SqlitePoolGuard { database_path: PathBuf, @@ -42,7 +52,7 @@ impl SqlitePoolGuard { } } - /// Close udnerlying sqlite pool + /// Close udnerlying sqlite pool and wait for files to be closed before returning. pub async fn close_pool(&self) { _ = self.close_pool_inner(); } @@ -50,7 +60,7 @@ impl SqlitePoolGuard { async fn close_pool_inner(&self) -> std::io::Result<()> { self.connection_pool.close().await; - if let Err(e) = self.wait_io_close().await { + if let Err(e) = self.wait_for_db_files_close().await { log::error!("Failed to wait for file to close: {e}"); } @@ -82,25 +92,17 @@ impl SqlitePoolGuard { database_files } - /// Wait for I/O close to the database files - /// - /// - macOS: uses `proc_pidinfo` (`sys/proc_info.h`) - /// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ - /// - /// - Linux, Android: uses `/proc/self/fd/` to list open file descriptors - /// See: https://stackoverflow.com/a/59797198/351305 - /// - /// - Windows: attempts to open files to detect whether they are still open. - async fn wait_io_close(&self) -> std::io::Result<()> { + /// Wait for database files to be closed before returning. + async fn wait_for_db_files_close(&self) -> std::io::Result<()> { let database_files = self.all_database_files(); let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); - for _ in 0..SQL_CLOSE_MAX_ATTEMPTS { - match Self::check_io_close(&paths) + for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS { + match imp::check_files_closed(&paths) .await - .inspect_err(|e| log::error!("check_io_close() failure: {e}")) + .inspect_err(|e| log::error!("imp::check_files_closed() failure: {e}")) { - Ok(false) | Err(_) => tokio::time::sleep(SQL_CLOSE_RETRY_DELAY).await, + Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await, Ok(true) => return Ok(()), } } @@ -110,95 +112,12 @@ impl SqlitePoolGuard { "timed out waiting for sqlite files to be closed", )) } - - /// Check if no more open file descriptors exist for the given files. - #[cfg(target_os = "macos")] - async fn check_io_close(file_paths: &[&Path]) -> io::Result { - let fd_list = proc_pidinfo_list_self::()?; - - for fd in fd_list - .iter() - .filter(|s| s.fd_type() == Ok(ProcFDType::VNODE)) - { - let Some(vnode) = proc_pidfdinfo_self::(fd.proc_fd) - .inspect_err(|e| { - log::warn!("proc_pidfdinfo_self::() failure: {e}"); - }) - .ok() - .flatten() - else { - continue; - }; - - if let Ok(true) = vnode - .path() - .map(|vnode_path| file_paths.contains(&vnode_path)) - .inspect_err(|e| { - log::warn!("vnode.path() failure: {e:?}"); - }) - { - return Ok(false); - } - } - - Ok(true) - } - - /// Check if no more open file descriptors exist for the given files. - #[cfg(any(target_os = "linux", target_os = "android"))] - async fn check_io_close(file_paths: &[&Path]) -> io::Result { - let mut dir = tokio::fs::read_dir("/proc/self/fd/").await?; - - while let Ok(Some(entry)) = dir.next_entry().await { - if entry - .file_type() - .await - .inspect_err(|e| log::warn!("entry.file_type() failure: {e}")) - .is_ok_and(|entry_type| entry_type.is_symlink()) - { - match tokio::fs::read_link(entry.path()).await { - Ok(resolved_path) => { - if file_paths.contains(&resolved_path.as_ref()) { - return Ok(false); - } - } - Err(e) => { - log::error!("Failed to read symlink: {e}"); - } - } - } - } - - Ok(true) - } - - #[cfg(windows)] - async fn check_io_close(file_paths: &[&Path]) -> io::Result { - // Error code returned when file is still in use. - const FILE_IN_USE_ERR: i32 = 32; - - for file_path in file_paths { - if let Err(e) = tokio::fs::OpenOptions::new() - .read(true) - .open(file_path) - .await - { - if e.raw_os_error() == Some(FILE_IN_USE_ERR) { - return Ok(false); - } else if e.kind() != io::ErrorKind::NotFound { - log::error!("Failed to open file: {}", file_path.display()); - } - } - } - - Ok(true) - } } #[cfg(test)] mod tests { use sqlx::{ - ConnectOptions, + ConnectOptions, Executor, sqlite::{SqliteAutoVacuum, SqliteSynchronous}, }; @@ -208,6 +127,9 @@ mod tests { async fn test_wait_close() { let temp_dir = tempfile::tempdir().unwrap(); let database_path = temp_dir.path().join("storage.sqlite"); + + println!("Database path: {}", database_path.display()); + let opts = sqlx::sqlite::SqliteConnectOptions::new() .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) @@ -217,10 +139,15 @@ mod tests { .disable_statement_logging(); let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap(); + connection_pool + .execute("create table test (col int)") + .await + .unwrap(); + let guard = SqlitePoolGuard::new(database_path, connection_pool); assert!( guard - .wait_io_close() + .wait_for_db_files_close() .await .err() .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut) diff --git a/sqlx-pool-guard/src/linux.rs b/sqlx-pool-guard/src/linux.rs new file mode 100644 index 0000000000..5a1fc94bd4 --- /dev/null +++ b/sqlx-pool-guard/src/linux.rs @@ -0,0 +1,34 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +static PROC_SELF_FD_DIR: &str = "/proc/self/fd/"; + +/// Check if there are no open file descriptors for the given files. +/// +/// Linux, Android: uses `/proc/self/fd/` to list open file descriptors +/// See: https://stackoverflow.com/a/59797198/351305 +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let mut dir = tokio::fs::read_dir(PROC_SELF_FD_DIR).await?; + + while let Ok(Some(entry)) = dir.next_entry().await { + if entry + .file_type() + .await + .inspect_err(|e| log::warn!("entry.file_type() failure: {e}")) + .is_ok_and(|entry_type| entry_type.is_symlink()) + { + match tokio::fs::read_link(entry.path()).await { + Ok(resolved_path) => { + if file_paths.contains(&resolved_path.as_ref()) { + return Ok(false); + } + } + Err(e) => { + log::error!("Failed to read symlink: {e}"); + } + } + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/macos.rs b/sqlx-pool-guard/src/macos.rs new file mode 100644 index 0000000000..1142ffd11b --- /dev/null +++ b/sqlx-pool-guard/src/macos.rs @@ -0,0 +1,41 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use proc_pidinfo::{ + ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, +}; + +/// Check if there are no open file descriptors for the given files. +/// +/// Uses `proc_pidinfo` (`sys/proc_info.h`) +/// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let fd_list = proc_pidinfo_list_self::()?; + + for fd in fd_list + .iter() + .filter(|s| s.fd_type() == Ok(ProcFDType::VNODE)) + { + let Some(vnode) = proc_pidfdinfo_self::(fd.proc_fd) + .inspect_err(|e| { + log::warn!("proc_pidfdinfo_self::() failure: {e}"); + }) + .ok() + .flatten() + else { + continue; + }; + + if let Ok(true) = vnode + .path() + .map(|vnode_path| file_paths.contains(&vnode_path)) + .inspect_err(|e| { + log::warn!("vnode.path() failure: {e:?}"); + }) + { + return Ok(false); + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs new file mode 100644 index 0000000000..43eee8f465 --- /dev/null +++ b/sqlx-pool-guard/src/windows.rs @@ -0,0 +1,174 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + ffi::{OsString, c_uchar, c_ulong, c_ushort, c_void}, + io, + os::windows::ffi::OsStringExt, + path::{Path, PathBuf}, +}; + +use windows::{ + Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS}, + Win32::{ + Foundation::{HANDLE, MAX_PATH, NTSTATUS, STATUS_INFO_LENGTH_MISMATCH}, + Storage::FileSystem::{ + FILE_NAME_NORMALIZED, FILE_TYPE_DISK, GetFileType, GetFinalPathNameByHandleW, + }, + System::{ + Memory::{ + GetProcessHeap, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc, + }, + Threading::GetCurrentProcessId, + }, + }, +}; + +/// Private information class used to retrieve open file handles +const SYSTEM_HANDLE_INFORMATION_CLASS: SYSTEM_INFORMATION_CLASS = SYSTEM_INFORMATION_CLASS(0x10); + +/// Initial buffer size holding the handle info +/// The number is based on what I observe on a pretty standard Windows 11 +const SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE: usize = 2_500_000; + +/// Check if there are no open handles to the given files. +/// +/// Uses undocumented NT API to obtain open handles on the system. +/// See: https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let current_pid = unsafe { GetCurrentProcessId() }; + + // Allocate info struct on heap with some initial value + let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE; + let mut handle_table_info = HeapGuard::::new(reserved_memory)?; + + let mut status: NTSTATUS = NTSTATUS::default(); + let mut return_len = reserved_memory as u32; + for _ in 0..2 { + status = unsafe { + NtQuerySystemInformation( + SYSTEM_HANDLE_INFORMATION_CLASS, + handle_table_info.inner as _, + return_len, + &mut return_len, + ) + }; + + // Buffer is too small, resize memory and retry again. + if status == STATUS_INFO_LENGTH_MISMATCH { + log::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); + println!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); + reserved_memory = return_len as usize; + handle_table_info.reallocate(reserved_memory)?; + } else { + break; + } + } + status.ok()?; + + let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; + let proc_entries = unsafe { + std::slice::from_raw_parts( + (*handle_table_info.inner).handles.as_ptr(), + num_handles as usize, + ) + }; + + for entry in proc_entries { + if entry.unique_process_id == current_pid { + let file_handle = HANDLE(entry.handle_value as _); + + if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK { + let mut file_handle_path = vec![0u16; MAX_PATH as usize]; + let num_chars_without_nul = unsafe { + GetFinalPathNameByHandleW( + file_handle, + &mut file_handle_path, + FILE_NAME_NORMALIZED, + ) as usize + }; + + if num_chars_without_nul > 0 { + let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]); + let file_handle_pathbuf = PathBuf::from(path_str); + + if file_paths.contains(&file_handle_pathbuf.as_path()) { + return Ok(false); + } + } + } + } + } + + Ok(true) +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleInformation { + pub number_of_handles: c_ulong, + pub handles: [SystemHandleTableEntryInfo; 1], +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleTableEntryInfo { + pub unique_process_id: c_ulong, + pub object_type_index: c_uchar, + pub handle_attributes: c_uchar, + pub handle_value: c_ushort, + pub object: *mut c_void, + pub granted_access: c_ulong, +} + +struct HeapGuard { + pub inner: *mut T, + process_heap: HANDLE, +} + +impl HeapGuard { + fn new(length: usize) -> io::Result { + let process_heap = unsafe { GetProcessHeap()? }; + let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ }; + + if inner.is_null() { + Err(io::Error::other("Failed to allocate memory")) + } else { + Ok(Self { + inner, + process_heap, + }) + } + } + + fn reallocate(&mut self, new_length: usize) -> io::Result<()> { + let new_ptr: *mut T = unsafe { + HeapReAlloc( + self.process_heap, + HEAP_ZERO_MEMORY, + Some(self.inner as _), + new_length, + ) as _ + }; + + if new_ptr.is_null() { + Err(io::Error::other("Failed to reallocate memory")) + } else { + self.inner = new_ptr; + Ok(()) + } + } +} + +impl Drop for HeapGuard { + fn drop(&mut self) { + unsafe { + HeapFree( + self.process_heap, + HEAP_FLAGS(0), + Some(self.inner as *mut c_void), + ) + } + .expect("HeapFree failure"); + } +} From 6391b7ed3af74c8250d4ff6ff1fe3be5e30f5464 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 29 May 2025 18:19:56 +0200 Subject: [PATCH 06/47] Document --- sqlx-pool-guard/src/windows.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs index 43eee8f465..9b04414804 100644 --- a/sqlx-pool-guard/src/windows.rs +++ b/sqlx-pool-guard/src/windows.rs @@ -42,6 +42,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE; let mut handle_table_info = HeapGuard::::new(reserved_memory)?; + // Request system handle information let mut status: NTSTATUS = NTSTATUS::default(); let mut return_len = reserved_memory as u32; for _ in 0..2 { @@ -66,6 +67,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { } status.ok()?; + // Convert returned data into slice let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; let proc_entries = unsafe { std::slice::from_raw_parts( @@ -74,11 +76,14 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { ) }; + // Iterate over open file handle entries for entry in proc_entries { if entry.unique_process_id == current_pid { let file_handle = HANDLE(entry.handle_value as _); + // Filter everything except disk files if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK { + // Obtain canonical path for file handle let mut file_handle_path = vec![0u16; MAX_PATH as usize]; let num_chars_without_nul = unsafe { GetFinalPathNameByHandleW( @@ -91,7 +96,6 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { if num_chars_without_nul > 0 { let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]); let file_handle_pathbuf = PathBuf::from(path_str); - if file_paths.contains(&file_handle_pathbuf.as_path()) { return Ok(false); } From e4e349bea8419810ba028bfda576552f0d6420dd Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 29 May 2025 18:21:29 +0200 Subject: [PATCH 07/47] Remove logs --- sqlx-pool-guard/src/lib.rs | 2 -- sqlx-pool-guard/src/windows.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index 8b120ee261..62838ac20f 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -128,8 +128,6 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let database_path = temp_dir.path().join("storage.sqlite"); - println!("Database path: {}", database_path.display()); - let opts = sqlx::sqlite::SqliteConnectOptions::new() .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs index 9b04414804..606b5863d5 100644 --- a/sqlx-pool-guard/src/windows.rs +++ b/sqlx-pool-guard/src/windows.rs @@ -58,7 +58,6 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { // Buffer is too small, resize memory and retry again. if status == STATUS_INFO_LENGTH_MISMATCH { log::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); - println!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); reserved_memory = return_len as usize; handle_table_info.reallocate(reserved_memory)?; } else { From 31e161604a18033f3f649a817924f12cd478fc0a Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 2 Jun 2025 17:36:24 +0200 Subject: [PATCH 08/47] Use sqlite pool guard --- Cargo.lock | 6 +- Cargo.toml | 5 +- common/client-core/surb-storage/Cargo.toml | 1 + .../src/backend/fs_backend/manager.rs | 55 ++++++++++--------- common/credential-storage/Cargo.toml | 10 +++- .../credential-storage/src/backends/sqlite.rs | 33 +++++------ .../src/persistent_storage/mod.rs | 10 +++- sqlx-pool-guard/src/{macos.rs => apple.rs} | 2 + sqlx-pool-guard/src/lib.rs | 23 ++++++-- sqlx-pool-guard/src/linux.rs | 2 + 10 files changed, 91 insertions(+), 56 deletions(-) rename sqlx-pool-guard/src/{macos.rs => apple.rs} (97%) diff --git a/Cargo.lock b/Cargo.lock index 01820eb102..508eb70645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5272,6 +5272,7 @@ dependencies = [ "nym-sphinx", "nym-task", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "time", "tokio", @@ -5472,6 +5473,7 @@ dependencies = [ "nym-ecash-time", "serde", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "tokio", "zeroize", @@ -7956,9 +7958,9 @@ dependencies = [ [[package]] name = "proc_pidinfo" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1724fdb3f7349e4c58806dfb29dbc3b5e2f8b0997bb5117acf8db6e555867a91" +checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a" dependencies = [ "libc", ] diff --git a/Cargo.toml b/Cargo.toml index e15ac95c90..f538f5db92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,8 @@ members = [ "service-providers/authenticator", "service-providers/common", "service-providers/ip-packet-router", - "service-providers/network-requester", "sqlx-pool-guard", + "service-providers/network-requester", + "sqlx-pool-guard", "tools/echo-server", "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", @@ -286,7 +287,7 @@ petgraph = "0.6.5" pin-project = "1.1" pin-project-lite = "0.2.16" publicsuffix = "2.3.0" -proc_pidinfo = "0.1" +proc_pidinfo = "0.1.3" quote = "1" rand = "0.8.5" rand_chacha = "0.3" diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 49dfbe4b8c..449ce0d589 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -17,6 +17,7 @@ tokio = { workspace = true, features = ["fs"] } nym-crypto = { path = "../../crypto", optional = true, default-features = false } nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task" } +sqlx-pool-guard = { path = "../../../sqlx-pool-guard" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] diff --git a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 3daa4744a4..68d21009c5 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -15,9 +15,11 @@ use sqlx::{ }; use std::path::Path; +use sqlx_pool_guard::SqlitePoolGuard; + #[derive(Debug, Clone)] pub struct StorageManager { - connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } // all SQL goes here @@ -37,7 +39,7 @@ impl StorageManager { .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) - .filename(database_path) + .filename(&database_path) .create_if_missing(fresh) .disable_statement_logging(); @@ -49,8 +51,11 @@ impl StorageManager { } }; + let connection_pool = + SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool); + if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") - .run(&connection_pool) + .run(&*connection_pool) .await { error!("Failed to initialize SQLx database: {err}"); @@ -70,35 +75,35 @@ impl StorageManager { #[allow(dead_code)] pub async fn status_table_exists(&self) -> Result { sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await .map(|r| r.is_some()) } pub async fn create_status_table(&self) -> Result<(), sqlx::Error> { sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_flush_status(&self) -> Result { sqlx::query!("SELECT flush_in_progress FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.flush_in_progress > 0) } pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> { sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_previous_flush_timestamp(&self) -> Result { sqlx::query!("SELECT previous_flush_timestamp FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.previous_flush_timestamp) } @@ -106,14 +111,14 @@ impl StorageManager { pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { let in_progress_int = i64::from(in_progress); sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_client_in_use_status(&self) -> Result { sqlx::query!("SELECT client_in_use FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.client_in_use > 0) } @@ -121,21 +126,21 @@ impl StorageManager { pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { let in_use_int = i64::from(in_use); sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM sender_tag;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_tags(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -147,21 +152,21 @@ impl StorageManager { stored_tag.recipient, stored_tag.tag ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_key;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_reply_keys(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -177,14 +182,14 @@ impl StorageManager { stored_reply_key.reply_key, stored_reply_key.sent_at_timestamp ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_surb_senders(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -199,7 +204,7 @@ impl StorageManager { stored_surb_sender.tag, stored_surb_sender.last_sent_timestamp ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .last_insert_rowid(); Ok(id) @@ -214,17 +219,17 @@ impl StorageManager { "SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?", sender_id ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_surb;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; sqlx::query!("DELETE FROM reply_surb_sender;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -241,7 +246,7 @@ impl StorageManager { stored_reply_surb.reply_surb_sender_id, stored_reply_surb.reply_surb ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -255,7 +260,7 @@ impl StorageManager { SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; "#, ) - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await } @@ -269,7 +274,7 @@ impl StorageManager { "#, metadata.min_reply_surb_threshold, metadata.max_reply_surb_threshold, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index b822d49db6..07a01486df 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -19,6 +19,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } nym-credentials = { path = "../credentials" } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } +sqlx-pool-guard = { path = "../../sqlx-pool-guard" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] @@ -31,8 +32,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [features] -persistent-storage = ["bincode", "serde"] \ No newline at end of file +persistent-storage = ["bincode", "serde"] diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 3d91741ee3..6ac45aec47 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -7,10 +7,11 @@ use crate::models::{ }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; +use sqlx_pool_guard::SqlitePoolGuard; #[derive(Clone)] pub struct SqliteEcashTicketbookManager { - connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } impl SqliteEcashTicketbookManager { @@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager { /// # Arguments /// /// * `connection_pool`: database connection pool to use. - pub fn new(connection_pool: sqlx::SqlitePool) -> Self { + pub fn new(connection_pool: SqlitePoolGuard) -> Self { SqliteEcashTicketbookManager { connection_pool } } @@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager { "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", deadline ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager { data, expiration_date, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager { epoch_id, total_tickets, used_tickets, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } @@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager { "#, ) .bind(data) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await? .is_some(); @@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager { FROM ecash_ticketbook "#, ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager { ticketbook_id, expected_current_total_spent ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .rows_affected(); Ok(affected > 0) @@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager { &self, ) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM pending_issuance") - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager { "DELETE FROM pending_issuance WHERE deposit_id = ?", pending_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager { "#, expiration_date ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, expiration_date ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index bccbccf4ab..c8dd5a08b2 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -37,6 +37,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use sqlx_pool_guard::SqlitePoolGuard; use std::path::Path; use zeroize::Zeroizing; @@ -62,7 +63,7 @@ impl PersistentStorage { .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) - .filename(database_path) + .filename(&database_path) .create_if_missing(true) .disable_statement_logging(); @@ -74,14 +75,17 @@ impl PersistentStorage { } }; - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + let connection_pool = + SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool); + + if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); connection_pool.close().await; return Err(err.into()); } Ok(PersistentStorage { - storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + storage_manager: SqliteEcashTicketbookManager::new(connection_pool), }) } } diff --git a/sqlx-pool-guard/src/macos.rs b/sqlx-pool-guard/src/apple.rs similarity index 97% rename from sqlx-pool-guard/src/macos.rs rename to sqlx-pool-guard/src/apple.rs index 1142ffd11b..549839a550 100644 --- a/sqlx-pool-guard/src/macos.rs +++ b/sqlx-pool-guard/src/apple.rs @@ -1,6 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::{io, path::Path}; + use proc_pidinfo::{ ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, }; diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index 62838ac20f..b851f1ef79 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -12,8 +12,8 @@ use std::{ #[path = "windows.rs"] mod imp; -#[cfg(target_os = "macos")] -#[path = "macos.rs"] +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "apple.rs"] mod imp; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -21,18 +21,25 @@ mod imp; mod imp; /// Max number of retry attempts -const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 10; +const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 20; /// Delay between file checks const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100); +/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217). +/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed. +#[derive(Debug, Clone)] pub struct SqlitePoolGuard { + /// Path to sqlite database file. database_path: PathBuf, + + /// Inner connection pool. connection_pool: sqlx::SqlitePool, } impl Deref for SqlitePoolGuard { type Target = sqlx::SqlitePool; + fn deref(&self) -> &Self::Target { &self.connection_pool } @@ -53,8 +60,12 @@ impl SqlitePoolGuard { } /// Close udnerlying sqlite pool and wait for files to be closed before returning. - pub async fn close_pool(&self) { - _ = self.close_pool_inner(); + pub async fn close(&self) { + // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. + if !self.connection_pool.is_closed() { + log::info!("Closing sqlite pool: {}", self.database_path.display()); + _ = self.close_pool_inner(); + } } async fn close_pool_inner(&self) -> std::io::Result<()> { @@ -126,7 +137,7 @@ mod tests { #[tokio::test] async fn test_wait_close() { let temp_dir = tempfile::tempdir().unwrap(); - let database_path = temp_dir.path().join("storage.sqlite"); + let database_path = temp_dir.path().join("storage.db"); let opts = sqlx::sqlite::SqliteConnectOptions::new() .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) diff --git a/sqlx-pool-guard/src/linux.rs b/sqlx-pool-guard/src/linux.rs index 5a1fc94bd4..f7610e67c5 100644 --- a/sqlx-pool-guard/src/linux.rs +++ b/sqlx-pool-guard/src/linux.rs @@ -1,6 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::{io, path::Path}; + static PROC_SELF_FD_DIR: &str = "/proc/self/fd/"; /// Check if there are no open file descriptors for the given files. From 574f7f1abdc4a67c9168b04a56a8303a28ddd44b Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 2 Jun 2025 17:41:56 +0200 Subject: [PATCH 09/47] Revert --- .../gateways-storage/src/backend/fs_backend/manager.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs index 0b7aafdb28..101a6a8710 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -47,14 +47,12 @@ impl StorageManager { StorageError::DatabaseConnectionError { source } })?; - if let Err(err) = sqlx::migrate!("./fs_gateways_migrations") + sqlx::migrate!("./fs_gateways_migrations") .run(&connection_pool) .await - { - error!("Failed to initialize SQLx database: {err}"); - connection_pool.close().await; - return Err(err)?; - } + .inspect_err(|err| { + error!("Failed to initialize SQLx database: {err}"); + })?; debug!("Database migration finished!"); Ok(StorageManager { connection_pool }) From 085103b33380fdaf883a4b071b6bf04bc4b3581b Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 2 Jun 2025 17:53:54 +0200 Subject: [PATCH 10/47] Cleanup --- .../client/base_client/non_wasm_helpers.rs | 49 ++----------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index 4f3a2c0c77..0400355fdc 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -7,15 +7,11 @@ use crate::{ config::Config, error::ClientCoreError, }; -#[cfg(windows)] -use log::debug; use log::{error, info, trace}; use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; -#[cfg(windows)] -use std::time::Duration; use std::{io, path::Path}; use time::OffsetDateTime; use url::Url; @@ -24,10 +20,7 @@ async fn setup_fresh_backend>( db_path: P, surb_config: &config::ReplySurbs, ) -> Result { - info!( - "creating fresh surb database: {}", - db_path.as_ref().display() - ); + info!("Creating fresh surb database"); let mut storage_backend = match fs_backend::Backend::init(db_path).await { Ok(backend) => backend, Err(err) => { @@ -79,7 +72,7 @@ async fn archive_corrupted_database>(db_path: P) -> io::Result<() }; let renamed = db_path.with_extension(new_extension); - rename_db_file(&db_path, &renamed).await.inspect_err(|_| { + tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| { tracing::error!( "Failed to rename corrupt database file: {} to {}", db_path.display(), @@ -88,42 +81,6 @@ async fn archive_corrupted_database>(db_path: P) -> io::Result<() }) } -#[cfg(not(windows))] -async fn rename_db_file(db_path: impl AsRef, renamed: impl AsRef) -> io::Result<()> { - tokio::fs::rename(db_path, &renamed).await -} - -#[cfg(windows)] -async fn rename_db_file(db_path: impl AsRef, renamed: impl AsRef) -> io::Result<()> { - // Due to bug in sqlx (https://github.com/launchbadge/sqlx/issues/3217), - // the sqlite file can be still in use after closing sqlite connection pool - // Poll for a bit until the db file is released. - - // Max number of retries - const MAX_RETRY_ATTEMPTS: u32 = 10; - // Delay between retries - const WAIT_DELAY: Duration = Duration::from_millis(100); - // Error code returned when file is still in use. - const FILE_IN_USE_ERR: i32 = 32; - - let mut retry_attempt = 0; - while let Err(e) = tokio::fs::rename(db_path.as_ref(), renamed.as_ref()).await { - retry_attempt += 1; - - if e.raw_os_error() == Some(FILE_IN_USE_ERR) && retry_attempt < MAX_RETRY_ATTEMPTS { - debug!( - "File {} is still open. Sleep and retry", - db_path.as_ref().display() - ); - tokio::time::sleep(WAIT_DELAY).await; - } else { - return Err(e); - } - } - - Ok(()) -} - pub async fn setup_fs_reply_surb_backend>( db_path: P, surb_config: &config::ReplySurbs, @@ -132,7 +89,7 @@ pub async fn setup_fs_reply_surb_backend>( // the existing one let db_path = db_path.as_ref(); if db_path.exists() { - info!("Loading existing surb database: {}", db_path.display()); + info!("Loading existing surb database"); match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await { Ok(backend) => Ok(backend), Err(err) => { From f26fd5384d910458f40b16b9c10bb56811c33e16 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 2 Jun 2025 18:27:07 +0200 Subject: [PATCH 11/47] Improve windows --- sqlx-pool-guard/src/windows.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs index 606b5863d5..4fca6c1bef 100644 --- a/sqlx-pool-guard/src/windows.rs +++ b/sqlx-pool-guard/src/windows.rs @@ -49,7 +49,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { status = unsafe { NtQuerySystemInformation( SYSTEM_HANDLE_INFORMATION_CLASS, - handle_table_info.inner as _, + handle_table_info.as_mut_ptr() as _, return_len, &mut return_len, ) @@ -70,7 +70,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; let proc_entries = unsafe { std::slice::from_raw_parts( - (*handle_table_info.inner).handles.as_ptr(), + (*handle_table_info.as_mut_ptr()).handles.as_ptr(), num_handles as usize, ) }; @@ -124,12 +124,14 @@ struct SystemHandleTableEntryInfo { pub granted_access: c_ulong, } +/// Managed heap memory struct HeapGuard { - pub inner: *mut T, + inner: *mut T, process_heap: HANDLE, } impl HeapGuard { + /// Allocate new memory using `HealAlloc` fn new(length: usize) -> io::Result { let process_heap = unsafe { GetProcessHeap()? }; let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ }; @@ -144,6 +146,10 @@ impl HeapGuard { } } + /// Reallocate existing chunk of memory + /// + /// On success: the internal memory pointer is replaced. + /// On failure: the internal memory pointer remains the same and still valid. fn reallocate(&mut self, new_length: usize) -> io::Result<()> { let new_ptr: *mut T = unsafe { HeapReAlloc( @@ -161,10 +167,15 @@ impl HeapGuard { Ok(()) } } + + fn as_mut_ptr(&self) -> *mut T { + self.inner + } } impl Drop for HeapGuard { fn drop(&mut self) { + #[allow(clippy::expect_used)] unsafe { HeapFree( self.process_heap, From 11262836d22ce1c8724925ccb5a06f1e4b5c0b28 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Mon, 2 Jun 2025 18:59:17 +0200 Subject: [PATCH 12/47] Clean up --- common/gateway-stats-storage/src/lib.rs | 1 - common/gateway-storage/src/lib.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 10aa61062d..8ad34bbca4 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -59,7 +59,6 @@ impl PersistentStatsStorage { if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); - connection_pool.close().await; return Err(err.into()); } diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 1d91576ccb..2d05d43fe7 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -108,7 +108,6 @@ impl GatewayStorage { if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); - connection_pool.close().await; return Err(err.into()); } From 02909c03dd1028036b404777cc09d7dd1cb69ab5 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Tue, 3 Jun 2025 14:49:35 +0200 Subject: [PATCH 13/47] Expose database path --- sqlx-pool-guard/src/lib.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index b851f1ef79..6f01b2b55c 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -37,21 +37,8 @@ pub struct SqlitePoolGuard { connection_pool: sqlx::SqlitePool, } -impl Deref for SqlitePoolGuard { - type Target = sqlx::SqlitePool; - - fn deref(&self) -> &Self::Target { - &self.connection_pool - } -} - -impl DerefMut for SqlitePoolGuard { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.connection_pool - } -} - impl SqlitePoolGuard { + /// Create new instance providing path to database and connection pool pub fn new(database_path: PathBuf, connection_pool: sqlx::SqlitePool) -> Self { Self { database_path, @@ -59,6 +46,11 @@ impl SqlitePoolGuard { } } + /// Returns database path + pub fn database_path(&self) -> &Path { + &self.database_path + } + /// Close udnerlying sqlite pool and wait for files to be closed before returning. pub async fn close(&self) { // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. @@ -125,6 +117,19 @@ impl SqlitePoolGuard { } } +impl Deref for SqlitePoolGuard { + type Target = sqlx::SqlitePool; + + fn deref(&self) -> &Self::Target { + &self.connection_pool + } +} + +impl DerefMut for SqlitePoolGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.connection_pool + } +} #[cfg(test)] mod tests { use sqlx::{ From b8c8d33c945c2d79eb86917380ecea150f8b8a2c Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Tue, 3 Jun 2025 15:12:55 +0200 Subject: [PATCH 14/47] Use log here --- common/client-core/src/client/base_client/non_wasm_helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index 0400355fdc..7a8b9b1467 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -73,7 +73,7 @@ async fn archive_corrupted_database>(db_path: P) -> io::Result<() let renamed = db_path.with_extension(new_extension); tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| { - tracing::error!( + error!( "Failed to rename corrupt database file: {} to {}", db_path.display(), renamed.display() From 7fcc188041f7776eaeb7d1d9f68b1cc3edad55f2 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Tue, 3 Jun 2025 17:19:42 +0200 Subject: [PATCH 15/47] Switch to tracing --- Cargo.lock | 2 +- sqlx-pool-guard/Cargo.toml | 2 +- sqlx-pool-guard/src/apple.rs | 4 ++-- sqlx-pool-guard/src/lib.rs | 8 ++++---- sqlx-pool-guard/src/linux.rs | 4 ++-- sqlx-pool-guard/src/windows.rs | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 508eb70645..08fc8c0c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9540,11 +9540,11 @@ dependencies = [ name = "sqlx-pool-guard" version = "0.1.0" dependencies = [ - "log", "proc_pidinfo", "sqlx", "tempfile", "tokio", + "tracing", "windows 0.61.1", ] diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index a77ee80024..86c57a5277 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -15,7 +15,7 @@ tokio = { workspace = true, features = [ "fs", ] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } -log.workspace = true +tracing.workspace = true [target.'cfg(target_os = "macos")'.dependencies] proc_pidinfo.workspace = true diff --git a/sqlx-pool-guard/src/apple.rs b/sqlx-pool-guard/src/apple.rs index 549839a550..a67ed1a447 100644 --- a/sqlx-pool-guard/src/apple.rs +++ b/sqlx-pool-guard/src/apple.rs @@ -20,7 +20,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { { let Some(vnode) = proc_pidfdinfo_self::(fd.proc_fd) .inspect_err(|e| { - log::warn!("proc_pidfdinfo_self::() failure: {e}"); + tracing::warn!("proc_pidfdinfo_self::() failure: {e}"); }) .ok() .flatten() @@ -32,7 +32,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { .path() .map(|vnode_path| file_paths.contains(&vnode_path)) .inspect_err(|e| { - log::warn!("vnode.path() failure: {e:?}"); + tracing::warn!("vnode.path() failure: {e:?}"); }) { return Ok(false); diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index 6f01b2b55c..fc5169b519 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -55,7 +55,7 @@ impl SqlitePoolGuard { pub async fn close(&self) { // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. if !self.connection_pool.is_closed() { - log::info!("Closing sqlite pool: {}", self.database_path.display()); + tracing::info!("Closing sqlite pool: {}", self.database_path.display()); _ = self.close_pool_inner(); } } @@ -64,7 +64,7 @@ impl SqlitePoolGuard { self.connection_pool.close().await; if let Err(e) = self.wait_for_db_files_close().await { - log::error!("Failed to wait for file to close: {e}"); + tracing::error!("Failed to wait for file to close: {e}"); } Ok(()) @@ -77,7 +77,7 @@ impl SqlitePoolGuard { .database_path .canonicalize() .inspect_err(|e| { - log::error!( + tracing::error!( "Failed to canonicalize path: {}. Cause: {e}", self.database_path.display() ); @@ -103,7 +103,7 @@ impl SqlitePoolGuard { for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS { match imp::check_files_closed(&paths) .await - .inspect_err(|e| log::error!("imp::check_files_closed() failure: {e}")) + .inspect_err(|e| tracing::error!("imp::check_files_closed() failure: {e}")) { Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await, Ok(true) => return Ok(()), diff --git a/sqlx-pool-guard/src/linux.rs b/sqlx-pool-guard/src/linux.rs index f7610e67c5..e0f26de9e5 100644 --- a/sqlx-pool-guard/src/linux.rs +++ b/sqlx-pool-guard/src/linux.rs @@ -16,7 +16,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { if entry .file_type() .await - .inspect_err(|e| log::warn!("entry.file_type() failure: {e}")) + .inspect_err(|e| tracing::warn!("entry.file_type() failure: {e}")) .is_ok_and(|entry_type| entry_type.is_symlink()) { match tokio::fs::read_link(entry.path()).await { @@ -26,7 +26,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { } } Err(e) => { - log::error!("Failed to read symlink: {e}"); + tracing::error!("Failed to read symlink: {e}"); } } } diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs index 4fca6c1bef..799eac2abe 100644 --- a/sqlx-pool-guard/src/windows.rs +++ b/sqlx-pool-guard/src/windows.rs @@ -57,7 +57,7 @@ pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { // Buffer is too small, resize memory and retry again. if status == STATUS_INFO_LENGTH_MISMATCH { - log::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); + tracing::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); reserved_memory = return_len as usize; handle_table_info.reallocate(reserved_memory)?; } else { From d7779df1b7c2d6248a260335de9f33429446895f Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Wed, 4 Jun 2025 10:58:08 +0200 Subject: [PATCH 16/47] Include proc_pidinfo on iOS --- sqlx-pool-guard/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index 86c57a5277..c576f522a0 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -17,7 +17,7 @@ tokio = { workspace = true, features = [ sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } tracing.workspace = true -[target.'cfg(target_os = "macos")'.dependencies] +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] proc_pidinfo.workspace = true [target.'cfg(windows)'.dependencies] From f5846d5bc2fda280dcacf80ea1fc23e07db0d2c6 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Wed, 4 Jun 2025 11:40:56 +0200 Subject: [PATCH 17/47] Log all tracing output just in case --- Cargo.lock | 1 + sqlx-pool-guard/Cargo.toml | 3 ++- sqlx-pool-guard/src/lib.rs | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 08fc8c0c70..efc17902f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9545,6 +9545,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "tracing-subscriber", "windows 0.61.1", ] diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index c576f522a0..5482346200 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -31,4 +31,5 @@ windows = { version = "0.61", features = [ ] } [dev-dependencies] -tempfile = { workspace = true } +tempfile.workspace = true +tracing-subscriber.workspace = true diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index fc5169b519..d9d89eaae7 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -141,6 +141,10 @@ mod tests { #[tokio::test] async fn test_wait_close() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + let temp_dir = tempfile::tempdir().unwrap(); let database_path = temp_dir.path().join("storage.db"); From 3593631e4ab631d9d03a5487a1af9934c3542b88 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Fri, 6 Jun 2025 13:24:04 +0200 Subject: [PATCH 18/47] Exclude sqlx-pool-guard from wasm builds --- common/client-core/surb-storage/Cargo.toml | 5 +++-- common/credential-storage/Cargo.toml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 449ce0d589..1706008302 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -17,14 +17,15 @@ tokio = { workspace = true, features = ["fs"] } nym-crypto = { path = "../../crypto", optional = true, default-features = false } nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task" } -sqlx-pool-guard = { path = "../../../sqlx-pool-guard" } - [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] optional = true +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../../sqlx-pool-guard" + [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 07a01486df..aa9b615805 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -19,8 +19,9 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } nym-credentials = { path = "../credentials" } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } -sqlx-pool-guard = { path = "../../sqlx-pool-guard" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../sqlx-pool-guard" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true From 9d82d6d1112680c4fbfed75e53a0e66e93ae8aff Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Fri, 6 Jun 2025 13:34:56 +0200 Subject: [PATCH 19/47] Hide tokio and sqlx behind not(wasm32) --- sqlx-pool-guard/Cargo.toml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml index 5482346200..cef202e4df 100644 --- a/sqlx-pool-guard/Cargo.toml +++ b/sqlx-pool-guard/Cargo.toml @@ -8,15 +8,16 @@ license.workspace = true workspace = true [dependencies] -tokio = { workspace = true, features = [ - "rt-multi-thread", - "macros", - "time", - "fs", -] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } tracing.workspace = true +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +workspace = true +features = ["runtime-tokio-rustls", "sqlite"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["rt-multi-thread", "macros", "time", "fs"] + [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] proc_pidinfo.workspace = true From e52bd918fb6a4940c4621907ee68327a3462876c Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Fri, 6 Jun 2025 15:00:40 +0200 Subject: [PATCH 20/47] Hide tokio behind feature --- common/client-core/surb-storage/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 1706008302..303675a220 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -12,12 +12,15 @@ dashmap.workspace = true log.workspace = true thiserror.workspace = true time.workspace = true -tokio = { workspace = true, features = ["fs"] } nym-crypto = { path = "../../crypto", optional = true, default-features = false } nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["fs"] + [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] From 3ac58e0c4980666010fae89428f61be665fae1c5 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 11 Jun 2025 16:02:19 +0200 Subject: [PATCH 21/47] Clean up remove old explorer references --- .github/workflows/ci-build.yml | 1 - .github/workflows/publish-nym-binaries.yml | 8 +- common/network-defaults/build.rs | 1 - common/network-defaults/src/mainnet.rs | 3 - common/network-defaults/src/network.rs | 11 - common/network-defaults/src/var_names.rs | 1 - envs/canary.env | 1 - envs/mainnet-local-api.env | 1 - envs/mainnet.env | 1 - envs/qa.env | 1 - envs/sandbox.env | 1 - explorer-nextjs/.eslintrc.json | 3 - explorer-nextjs/.gitignore | 36 - explorer-nextjs/README.md | 36 - explorer-nextjs/app/App.tsx | 11 - explorer-nextjs/app/account/[id]/page.tsx | 407 - explorer-nextjs/app/api/constants.ts | 39 - explorer-nextjs/app/api/index.ts | 217 - explorer-nextjs/app/assets/world-110m.json | 39718 ---------------- .../app/components/ComponentError.tsx | 12 - .../app/components/ContentCard.tsx | 38 - .../app/components/CustomColumnHeading.tsx | 30 - .../Delegations/ConfirmationModal.tsx | 83 - .../Delegations/DelegateIconButton.tsx | 39 - .../components/Delegations/DelegateModal.tsx | 191 - .../Delegations/DelegationModal.tsx | 95 - .../app/components/Delegations/ErrorModal.tsx | 28 - .../components/Delegations/LoadingModal.tsx | 18 - .../components/Delegations/ModalDivider.tsx | 6 - .../components/Delegations/ModalListItem.tsx | 32 - .../components/Delegations/SimpleModal.tsx | 152 - .../app/components/Delegations/index.ts | 10 - .../app/components/Delegations/styles.ts | 21 - .../app/components/DetailTable.tsx | 146 - .../app/components/Filters/Filters.tsx | 193 - .../app/components/Filters/FiltersButton.tsx | 34 - .../app/components/Filters/filterSchema.ts | 69 - explorer-nextjs/app/components/Footer.tsx | 56 - .../app/components/Gateways/Gateways.ts | 52 - .../Gateways/VersionDisplaySelector.tsx | 51 - explorer-nextjs/app/components/Icons.ts | 28 - .../app/components/MixNodes/BondBreakdown.tsx | 213 - .../app/components/MixNodes/DetailSection.tsx | 114 - .../components/MixNodes/Economics/Columns.ts | 51 - .../Economics/EconomicsProgress.stories.tsx | 31 - .../MixNodes/Economics/EconomicsProgress.tsx | 38 - .../Economics/MixNodeEconomics.stories.tsx | 107 - .../app/components/MixNodes/Economics/Rows.ts | 57 - .../Economics/StakeSaturationProgressBar.tsx | 47 - .../components/MixNodes/Economics/Table.tsx | 91 - .../components/MixNodes/Economics/index.ts | 3 - .../components/MixNodes/Economics/types.ts | 20 - .../app/components/MixNodes/Status.tsx | 36 - .../components/MixNodes/StatusDropdown.tsx | 83 - .../app/components/MixNodes/index.ts | 3 - .../app/components/MixNodes/mappings.ts | 57 - .../app/components/Nav/DesktopNav.tsx | 379 - .../app/components/Nav/MobileNav.tsx | 147 - explorer-nextjs/app/components/Nav/Navbar.tsx | 18 - explorer-nextjs/app/components/Nav/Search.tsx | 76 - .../app/components/NetworkTitle.tsx | 57 - .../app/components/ReleaseAlert.tsx | 9 - explorer-nextjs/app/components/Socials.tsx | 58 - explorer-nextjs/app/components/StatsCard.tsx | 73 - explorer-nextjs/app/components/StyledLink.tsx | 34 - explorer-nextjs/app/components/Switch.tsx | 70 - .../app/components/TableToolbar.tsx | 61 - explorer-nextjs/app/components/Title.tsx | 14 - explorer-nextjs/app/components/Tooltip.tsx | 36 - .../app/components/TwoColSmallTable.tsx | 78 - .../app/components/Universal-DataGrid.tsx | 96 - .../app/components/UptimeChart.tsx | 115 - .../components/Wallet/ConnectKeplrWallet.tsx | 50 - .../app/components/Wallet/WalletAddress.tsx | 20 - .../app/components/Wallet/WalletBalance.tsx | 25 - .../app/components/Wallet/index.ts | 2 - explorer-nextjs/app/components/WorldMap.tsx | 129 - explorer-nextjs/app/components/index.ts | 9 - explorer-nextjs/app/context/cosmos-kit.tsx | 73 - explorer-nextjs/app/context/delegations.tsx | 252 - explorer-nextjs/app/context/gateway.tsx | 69 - explorer-nextjs/app/context/hooks.ts | 49 - explorer-nextjs/app/context/main.tsx | 297 - explorer-nextjs/app/context/mixnode.tsx | 173 - explorer-nextjs/app/context/nav.tsx | 59 - explorer-nextjs/app/context/node.tsx | 77 - explorer-nextjs/app/context/wallet.tsx | 123 - explorer-nextjs/app/delegations/page.tsx | 311 - .../app/errors/ErrorBoundaryContent.tsx | 26 - explorer-nextjs/app/hooks/index.ts | 4 - .../app/hooks/useGetMixnodeStatusColor.ts | 19 - explorer-nextjs/app/hooks/useIsMobile.ts | 11 - explorer-nextjs/app/hooks/useIsMounted.ts | 16 - explorer-nextjs/app/hooks/useNymClient.tsx | 44 - explorer-nextjs/app/icons/DelevateSVG.tsx | 12 - explorer-nextjs/app/icons/ElipsSVG.tsx | 20 - explorer-nextjs/app/icons/GatewaysSVG.tsx | 28 - explorer-nextjs/app/icons/LightSwitchSVG.tsx | 15 - explorer-nextjs/app/icons/MixnodesSVG.tsx | 92 - .../app/icons/MobileDrawerClose.tsx | 10 - explorer-nextjs/app/icons/NetworksSVG.tsx | 18 - explorer-nextjs/app/icons/NodemapSVG.tsx | 22 - explorer-nextjs/app/icons/NymVpn.tsx | 58 - explorer-nextjs/app/icons/OverviewSVG.tsx | 16 - explorer-nextjs/app/icons/TokenSVG.tsx | 25 - explorer-nextjs/app/icons/ValidatorsSVG.tsx | 47 - .../app/icons/socials/DiscordIcon.tsx | 38 - .../app/icons/socials/GitHubIcon.tsx | 32 - .../app/icons/socials/TelegramIcon.tsx | 23 - .../app/icons/socials/TwitterIcon.tsx | 30 - explorer-nextjs/app/layout.tsx | 21 - explorer-nextjs/app/loading.tsx | 10 - .../network-components/gateways/[id]/page.tsx | 206 - .../app/network-components/gateways/page.tsx | 256 - .../network-components/mixnodes/[id]/page.tsx | 302 - .../app/network-components/mixnodes/page.tsx | 382 - .../network-components/nodes/DeclaredRole.tsx | 11 - .../nodes/[id]/NodeDelegationsTable.tsx | 93 - .../network-components/nodes/[id]/page.tsx | 336 - .../app/network-components/nodes/page.tsx | 254 - explorer-nextjs/app/nodemap/page.tsx | 85 - explorer-nextjs/app/page.tsx | 144 - explorer-nextjs/app/providers/index.tsx | 19 - explorer-nextjs/app/theme/index.tsx | 15 - explorer-nextjs/app/theme/mui-theme.d.ts | 36 - explorer-nextjs/app/typeDefs/explorer-api.ts | 294 - explorer-nextjs/app/typeDefs/filters.ts | 22 - explorer-nextjs/app/typeDefs/network.ts | 1 - explorer-nextjs/app/typeDefs/tables.ts | 8 - explorer-nextjs/app/typings/FC.d.ts | 1 - explorer-nextjs/app/typings/jpeg.d.ts | 9 - explorer-nextjs/app/typings/json.d.ts | 4 - explorer-nextjs/app/typings/png.d.ts | 4 - .../app/typings/react-identicons.d.ts | 18 - .../app/typings/react-tooltip.d.ts | 1 - explorer-nextjs/app/typings/svg.d.ts | 4 - explorer-nextjs/app/utils/currency.ts | 110 - explorer-nextjs/app/utils/index.ts | 124 - explorer-nextjs/next.config.mjs | 14 - explorer-nextjs/package.json | 31 - explorer-nextjs/public/favicon.ico | Bin 5430 -> 0 bytes explorer-nextjs/public/next.svg | 1 - explorer-nextjs/public/vercel.svg | 1 - explorer-nextjs/tsconfig.json | 43 - explorer/.babelrc | 3 - explorer/.editorconfig | 18 - explorer/.env.dev | 9 - explorer/.env.prod | 6 - explorer/.env.qa | 6 - explorer/.eslintrc.js | 14 - explorer/.gitignore | 4 - explorer/.nvmrc | 1 - explorer/.prettierrc | 6 - explorer/.storybook/main.js | 63 - explorer/.storybook/preview.js | 56 - explorer/.vscode/settings.json | 5 - explorer/CHANGELOG.md | 47 - explorer/README.md | 77 - explorer/docs/README.md | 48 - explorer/jest.config.js | 5 - explorer/package.json | 146 - explorer/src/App.tsx | 22 - explorer/src/api/constants.ts | 31 - explorer/src/api/index.ts | 173 - explorer/src/assets/world-110m.json | 39718 ---------------- explorer/src/components/ComponentError.tsx | 12 - explorer/src/components/ContentCard.tsx | 40 - .../src/components/CustomColumnHeading.tsx | 30 - .../Delegations/ConfirmationModal.tsx | 83 - .../Delegations/DelegateIconButton.tsx | 33 - .../components/Delegations/DelegateModal.tsx | 166 - .../Delegations/DelegationModal.tsx | 67 - .../src/components/Delegations/ErrorModal.tsx | 28 - .../components/Delegations/LoadingModal.tsx | 18 - .../components/Delegations/ModalDivider.tsx | 6 - .../components/Delegations/ModalListItem.tsx | 32 - .../components/Delegations/SimpleModal.tsx | 112 - explorer/src/components/Delegations/index.ts | 10 - explorer/src/components/Delegations/styles.ts | 21 - explorer/src/components/DetailTable.tsx | 127 - explorer/src/components/Filters/Filters.tsx | 172 - .../src/components/Filters/FiltersButton.tsx | 34 - .../src/components/Filters/filterSchema.ts | 69 - explorer/src/components/Footer.tsx | 53 - explorer/src/components/Gateways.ts | 52 - .../Gateways/VersionDisplaySelector.tsx | 42 - explorer/src/components/Icons.ts | 28 - .../src/components/MixNodes/BondBreakdown.tsx | 206 - .../src/components/MixNodes/DetailSection.tsx | 95 - .../components/MixNodes/Economics/Columns.ts | 51 - .../Economics/EconomicsProgress.stories.tsx | 31 - .../MixNodes/Economics/EconomicsProgress.tsx | 38 - .../Economics/MixNodeEconomics.stories.tsx | 107 - .../src/components/MixNodes/Economics/Rows.ts | 57 - .../Economics/StakeSaturationProgressBar.tsx | 32 - .../components/MixNodes/Economics/Table.tsx | 74 - .../components/MixNodes/Economics/index.ts | 3 - .../components/MixNodes/Economics/types.ts | 20 - explorer/src/components/MixNodes/Status.tsx | 34 - .../components/MixNodes/StatusDropdown.tsx | 77 - explorer/src/components/MixNodes/index.ts | 3 - explorer/src/components/MixNodes/mappings.ts | 57 - explorer/src/components/MobileNav.tsx | 128 - explorer/src/components/Nav.tsx | 401 - explorer/src/components/NetworkTitle.tsx | 47 - explorer/src/components/Socials.tsx | 40 - explorer/src/components/StatsCard.tsx | 67 - explorer/src/components/StyledLink.tsx | 28 - explorer/src/components/Switch.tsx | 70 - explorer/src/components/TableToolbar.tsx | 102 - explorer/src/components/Title.tsx | 14 - explorer/src/components/Tooltip.tsx | 36 - explorer/src/components/TwoColSmallTable.tsx | 78 - .../src/components/Universal-DataGrid.tsx | 96 - explorer/src/components/UptimeChart.tsx | 115 - .../components/Wallet/ConnectKeplrWallet.tsx | 43 - .../src/components/Wallet/WalletAddress.tsx | 20 - .../src/components/Wallet/WalletBalance.tsx | 25 - explorer/src/components/Wallet/index.ts | 2 - explorer/src/components/WorldMap.tsx | 113 - .../src/components/delegatorsInfo/types.ts | 15 - explorer/src/components/index.ts | 9 - explorer/src/context/cosmos-kit.tsx | 71 - explorer/src/context/delegations.tsx | 200 - explorer/src/context/gateway.tsx | 59 - explorer/src/context/hooks.ts | 47 - explorer/src/context/main.tsx | 241 - explorer/src/context/mixnode.tsx | 157 - explorer/src/context/nav.tsx | 60 - explorer/src/context/wallet.tsx | 90 - explorer/src/errors/ErrorBoundaryContent.tsx | 24 - explorer/src/hooks/index.ts | 4 - .../src/hooks/useGetMixnodeStatusColor.ts | 17 - explorer/src/hooks/useIsMobile.ts | 9 - explorer/src/hooks/useIsMounted.ts | 14 - explorer/src/hooks/useNymClient.tsx | 31 - explorer/src/icons/DelevateSVG.tsx | 12 - explorer/src/icons/ElipsSVG.tsx | 20 - explorer/src/icons/GatewaysSVG.tsx | 28 - explorer/src/icons/LightSwitchSVG.tsx | 15 - explorer/src/icons/MixnodesSVG.tsx | 92 - explorer/src/icons/MobileDrawerClose.tsx | 10 - explorer/src/icons/NetworksSVG.tsx | 18 - explorer/src/icons/NodemapSVG.tsx | 22 - explorer/src/icons/NymVpn.tsx | 56 - explorer/src/icons/OverviewSVG.tsx | 16 - explorer/src/icons/TokenSVG.tsx | 25 - explorer/src/icons/ValidatorsSVG.tsx | 47 - explorer/src/icons/socials/DiscordIcon.tsx | 40 - explorer/src/icons/socials/GitHubIcon.tsx | 34 - explorer/src/icons/socials/TelegramIcon.tsx | 25 - explorer/src/icons/socials/TwitterIcon.tsx | 32 - explorer/src/index.html | 14 - explorer/src/index.tsx | 33 - explorer/src/pages/404/index.tsx | 49 - explorer/src/pages/Delegations/index.tsx | 264 - explorer/src/pages/GatewayDetail/index.tsx | 184 - explorer/src/pages/Gateways/index.tsx | 271 - explorer/src/pages/MixnodeDetail/index.tsx | 236 - explorer/src/pages/Mixnodes/index.tsx | 427 - explorer/src/pages/MixnodesMap/index.tsx | 114 - explorer/src/pages/Overview/index.tsx | 130 - explorer/src/pages/ServiceProviders/index.tsx | 135 - explorer/src/routes/index.tsx | 17 - explorer/src/routes/network-components.tsx | 27 - explorer/src/stories/Introduction.stories.mdx | 7 - explorer/src/styles.css | 30 - explorer/src/tests/Nav.test.tsx | 13 - explorer/src/tests/WorldMap.test.tsx | 28 - explorer/src/tests/todo.test.ts | 7 - explorer/src/theme/index.tsx | 8 - explorer/src/theme/mui-theme.d.ts | 37 - explorer/src/typeDefs/explorer-api.ts | 277 - explorer/src/typeDefs/filters.ts | 22 - explorer/src/typeDefs/network.ts | 1 - explorer/src/typeDefs/tables.ts | 8 - explorer/src/typings/FC.d.ts | 1 - explorer/src/typings/jpeg.d.ts | 9 - explorer/src/typings/json.d.ts | 4 - explorer/src/typings/png.d.ts | 4 - explorer/src/typings/react-identicons.d.ts | 18 - explorer/src/typings/react-tooltip.d.ts | 1 - explorer/src/typings/svg.d.ts | 4 - explorer/src/utils/currency.ts | 100 - explorer/src/utils/index.ts | 123 - explorer/src/x-data-grid/LICENSE | 21 - explorer/src/x-data-grid/README.md | 29 - explorer/src/x-data-grid/package.json | 89 - explorer/tsconfig.json | 15 - explorer/webpack.common.js | 32 - explorer/webpack.config.js | 76 - explorer/webpack.prod.js | 52 - .../nym-node-status-api/src/cli/mod.rs | 4 - nym-wallet/nym-wallet-types/src/network/qa.rs | 3 - .../nym-wallet-types/src/network/sandbox.rs | 3 - .../testnet-manager/src/manager/network.rs | 1 - .../src/manager/network_init.rs | 1 - 297 files changed, 5 insertions(+), 97804 deletions(-) delete mode 100644 explorer-nextjs/.eslintrc.json delete mode 100644 explorer-nextjs/.gitignore delete mode 100644 explorer-nextjs/README.md delete mode 100644 explorer-nextjs/app/App.tsx delete mode 100644 explorer-nextjs/app/account/[id]/page.tsx delete mode 100644 explorer-nextjs/app/api/constants.ts delete mode 100644 explorer-nextjs/app/api/index.ts delete mode 100644 explorer-nextjs/app/assets/world-110m.json delete mode 100644 explorer-nextjs/app/components/ComponentError.tsx delete mode 100644 explorer-nextjs/app/components/ContentCard.tsx delete mode 100644 explorer-nextjs/app/components/CustomColumnHeading.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/DelegateModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/DelegationModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/ErrorModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/LoadingModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/ModalDivider.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/ModalListItem.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/SimpleModal.tsx delete mode 100644 explorer-nextjs/app/components/Delegations/index.ts delete mode 100644 explorer-nextjs/app/components/Delegations/styles.ts delete mode 100644 explorer-nextjs/app/components/DetailTable.tsx delete mode 100644 explorer-nextjs/app/components/Filters/Filters.tsx delete mode 100644 explorer-nextjs/app/components/Filters/FiltersButton.tsx delete mode 100644 explorer-nextjs/app/components/Filters/filterSchema.ts delete mode 100644 explorer-nextjs/app/components/Footer.tsx delete mode 100644 explorer-nextjs/app/components/Gateways/Gateways.ts delete mode 100644 explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx delete mode 100644 explorer-nextjs/app/components/Icons.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/DetailSection.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/Columns.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/Rows.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/Table.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/index.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/Economics/types.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/Status.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx delete mode 100644 explorer-nextjs/app/components/MixNodes/index.ts delete mode 100644 explorer-nextjs/app/components/MixNodes/mappings.ts delete mode 100644 explorer-nextjs/app/components/Nav/DesktopNav.tsx delete mode 100644 explorer-nextjs/app/components/Nav/MobileNav.tsx delete mode 100644 explorer-nextjs/app/components/Nav/Navbar.tsx delete mode 100644 explorer-nextjs/app/components/Nav/Search.tsx delete mode 100644 explorer-nextjs/app/components/NetworkTitle.tsx delete mode 100644 explorer-nextjs/app/components/ReleaseAlert.tsx delete mode 100644 explorer-nextjs/app/components/Socials.tsx delete mode 100644 explorer-nextjs/app/components/StatsCard.tsx delete mode 100644 explorer-nextjs/app/components/StyledLink.tsx delete mode 100644 explorer-nextjs/app/components/Switch.tsx delete mode 100644 explorer-nextjs/app/components/TableToolbar.tsx delete mode 100644 explorer-nextjs/app/components/Title.tsx delete mode 100644 explorer-nextjs/app/components/Tooltip.tsx delete mode 100644 explorer-nextjs/app/components/TwoColSmallTable.tsx delete mode 100644 explorer-nextjs/app/components/Universal-DataGrid.tsx delete mode 100644 explorer-nextjs/app/components/UptimeChart.tsx delete mode 100644 explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx delete mode 100644 explorer-nextjs/app/components/Wallet/WalletAddress.tsx delete mode 100644 explorer-nextjs/app/components/Wallet/WalletBalance.tsx delete mode 100644 explorer-nextjs/app/components/Wallet/index.ts delete mode 100644 explorer-nextjs/app/components/WorldMap.tsx delete mode 100644 explorer-nextjs/app/components/index.ts delete mode 100644 explorer-nextjs/app/context/cosmos-kit.tsx delete mode 100644 explorer-nextjs/app/context/delegations.tsx delete mode 100644 explorer-nextjs/app/context/gateway.tsx delete mode 100644 explorer-nextjs/app/context/hooks.ts delete mode 100644 explorer-nextjs/app/context/main.tsx delete mode 100644 explorer-nextjs/app/context/mixnode.tsx delete mode 100644 explorer-nextjs/app/context/nav.tsx delete mode 100644 explorer-nextjs/app/context/node.tsx delete mode 100644 explorer-nextjs/app/context/wallet.tsx delete mode 100644 explorer-nextjs/app/delegations/page.tsx delete mode 100644 explorer-nextjs/app/errors/ErrorBoundaryContent.tsx delete mode 100644 explorer-nextjs/app/hooks/index.ts delete mode 100644 explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts delete mode 100644 explorer-nextjs/app/hooks/useIsMobile.ts delete mode 100644 explorer-nextjs/app/hooks/useIsMounted.ts delete mode 100644 explorer-nextjs/app/hooks/useNymClient.tsx delete mode 100644 explorer-nextjs/app/icons/DelevateSVG.tsx delete mode 100644 explorer-nextjs/app/icons/ElipsSVG.tsx delete mode 100644 explorer-nextjs/app/icons/GatewaysSVG.tsx delete mode 100644 explorer-nextjs/app/icons/LightSwitchSVG.tsx delete mode 100644 explorer-nextjs/app/icons/MixnodesSVG.tsx delete mode 100644 explorer-nextjs/app/icons/MobileDrawerClose.tsx delete mode 100644 explorer-nextjs/app/icons/NetworksSVG.tsx delete mode 100644 explorer-nextjs/app/icons/NodemapSVG.tsx delete mode 100644 explorer-nextjs/app/icons/NymVpn.tsx delete mode 100644 explorer-nextjs/app/icons/OverviewSVG.tsx delete mode 100644 explorer-nextjs/app/icons/TokenSVG.tsx delete mode 100644 explorer-nextjs/app/icons/ValidatorsSVG.tsx delete mode 100644 explorer-nextjs/app/icons/socials/DiscordIcon.tsx delete mode 100644 explorer-nextjs/app/icons/socials/GitHubIcon.tsx delete mode 100644 explorer-nextjs/app/icons/socials/TelegramIcon.tsx delete mode 100644 explorer-nextjs/app/icons/socials/TwitterIcon.tsx delete mode 100644 explorer-nextjs/app/layout.tsx delete mode 100644 explorer-nextjs/app/loading.tsx delete mode 100644 explorer-nextjs/app/network-components/gateways/[id]/page.tsx delete mode 100644 explorer-nextjs/app/network-components/gateways/page.tsx delete mode 100644 explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx delete mode 100644 explorer-nextjs/app/network-components/mixnodes/page.tsx delete mode 100644 explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx delete mode 100644 explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx delete mode 100644 explorer-nextjs/app/network-components/nodes/[id]/page.tsx delete mode 100644 explorer-nextjs/app/network-components/nodes/page.tsx delete mode 100644 explorer-nextjs/app/nodemap/page.tsx delete mode 100644 explorer-nextjs/app/page.tsx delete mode 100644 explorer-nextjs/app/providers/index.tsx delete mode 100644 explorer-nextjs/app/theme/index.tsx delete mode 100644 explorer-nextjs/app/theme/mui-theme.d.ts delete mode 100644 explorer-nextjs/app/typeDefs/explorer-api.ts delete mode 100644 explorer-nextjs/app/typeDefs/filters.ts delete mode 100644 explorer-nextjs/app/typeDefs/network.ts delete mode 100644 explorer-nextjs/app/typeDefs/tables.ts delete mode 100644 explorer-nextjs/app/typings/FC.d.ts delete mode 100644 explorer-nextjs/app/typings/jpeg.d.ts delete mode 100644 explorer-nextjs/app/typings/json.d.ts delete mode 100644 explorer-nextjs/app/typings/png.d.ts delete mode 100644 explorer-nextjs/app/typings/react-identicons.d.ts delete mode 100644 explorer-nextjs/app/typings/react-tooltip.d.ts delete mode 100644 explorer-nextjs/app/typings/svg.d.ts delete mode 100644 explorer-nextjs/app/utils/currency.ts delete mode 100644 explorer-nextjs/app/utils/index.ts delete mode 100644 explorer-nextjs/next.config.mjs delete mode 100644 explorer-nextjs/package.json delete mode 100644 explorer-nextjs/public/favicon.ico delete mode 100644 explorer-nextjs/public/next.svg delete mode 100644 explorer-nextjs/public/vercel.svg delete mode 100644 explorer-nextjs/tsconfig.json delete mode 100644 explorer/.babelrc delete mode 100644 explorer/.editorconfig delete mode 100644 explorer/.env.dev delete mode 100644 explorer/.env.prod delete mode 100644 explorer/.env.qa delete mode 100644 explorer/.eslintrc.js delete mode 100644 explorer/.gitignore delete mode 100644 explorer/.nvmrc delete mode 100644 explorer/.prettierrc delete mode 100644 explorer/.storybook/main.js delete mode 100644 explorer/.storybook/preview.js delete mode 100644 explorer/.vscode/settings.json delete mode 100644 explorer/CHANGELOG.md delete mode 100644 explorer/README.md delete mode 100644 explorer/docs/README.md delete mode 100644 explorer/jest.config.js delete mode 100644 explorer/package.json delete mode 100644 explorer/src/App.tsx delete mode 100644 explorer/src/api/constants.ts delete mode 100644 explorer/src/api/index.ts delete mode 100644 explorer/src/assets/world-110m.json delete mode 100644 explorer/src/components/ComponentError.tsx delete mode 100644 explorer/src/components/ContentCard.tsx delete mode 100644 explorer/src/components/CustomColumnHeading.tsx delete mode 100644 explorer/src/components/Delegations/ConfirmationModal.tsx delete mode 100644 explorer/src/components/Delegations/DelegateIconButton.tsx delete mode 100644 explorer/src/components/Delegations/DelegateModal.tsx delete mode 100644 explorer/src/components/Delegations/DelegationModal.tsx delete mode 100644 explorer/src/components/Delegations/ErrorModal.tsx delete mode 100644 explorer/src/components/Delegations/LoadingModal.tsx delete mode 100644 explorer/src/components/Delegations/ModalDivider.tsx delete mode 100644 explorer/src/components/Delegations/ModalListItem.tsx delete mode 100644 explorer/src/components/Delegations/SimpleModal.tsx delete mode 100644 explorer/src/components/Delegations/index.ts delete mode 100644 explorer/src/components/Delegations/styles.ts delete mode 100644 explorer/src/components/DetailTable.tsx delete mode 100644 explorer/src/components/Filters/Filters.tsx delete mode 100644 explorer/src/components/Filters/FiltersButton.tsx delete mode 100644 explorer/src/components/Filters/filterSchema.ts delete mode 100644 explorer/src/components/Footer.tsx delete mode 100644 explorer/src/components/Gateways.ts delete mode 100644 explorer/src/components/Gateways/VersionDisplaySelector.tsx delete mode 100644 explorer/src/components/Icons.ts delete mode 100644 explorer/src/components/MixNodes/BondBreakdown.tsx delete mode 100644 explorer/src/components/MixNodes/DetailSection.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/Columns.ts delete mode 100644 explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/Rows.ts delete mode 100644 explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/Table.tsx delete mode 100644 explorer/src/components/MixNodes/Economics/index.ts delete mode 100644 explorer/src/components/MixNodes/Economics/types.ts delete mode 100644 explorer/src/components/MixNodes/Status.tsx delete mode 100644 explorer/src/components/MixNodes/StatusDropdown.tsx delete mode 100644 explorer/src/components/MixNodes/index.ts delete mode 100644 explorer/src/components/MixNodes/mappings.ts delete mode 100644 explorer/src/components/MobileNav.tsx delete mode 100644 explorer/src/components/Nav.tsx delete mode 100644 explorer/src/components/NetworkTitle.tsx delete mode 100644 explorer/src/components/Socials.tsx delete mode 100644 explorer/src/components/StatsCard.tsx delete mode 100644 explorer/src/components/StyledLink.tsx delete mode 100644 explorer/src/components/Switch.tsx delete mode 100644 explorer/src/components/TableToolbar.tsx delete mode 100644 explorer/src/components/Title.tsx delete mode 100644 explorer/src/components/Tooltip.tsx delete mode 100644 explorer/src/components/TwoColSmallTable.tsx delete mode 100644 explorer/src/components/Universal-DataGrid.tsx delete mode 100644 explorer/src/components/UptimeChart.tsx delete mode 100644 explorer/src/components/Wallet/ConnectKeplrWallet.tsx delete mode 100644 explorer/src/components/Wallet/WalletAddress.tsx delete mode 100644 explorer/src/components/Wallet/WalletBalance.tsx delete mode 100644 explorer/src/components/Wallet/index.ts delete mode 100644 explorer/src/components/WorldMap.tsx delete mode 100644 explorer/src/components/delegatorsInfo/types.ts delete mode 100644 explorer/src/components/index.ts delete mode 100644 explorer/src/context/cosmos-kit.tsx delete mode 100644 explorer/src/context/delegations.tsx delete mode 100644 explorer/src/context/gateway.tsx delete mode 100644 explorer/src/context/hooks.ts delete mode 100644 explorer/src/context/main.tsx delete mode 100644 explorer/src/context/mixnode.tsx delete mode 100644 explorer/src/context/nav.tsx delete mode 100644 explorer/src/context/wallet.tsx delete mode 100644 explorer/src/errors/ErrorBoundaryContent.tsx delete mode 100644 explorer/src/hooks/index.ts delete mode 100644 explorer/src/hooks/useGetMixnodeStatusColor.ts delete mode 100644 explorer/src/hooks/useIsMobile.ts delete mode 100644 explorer/src/hooks/useIsMounted.ts delete mode 100644 explorer/src/hooks/useNymClient.tsx delete mode 100644 explorer/src/icons/DelevateSVG.tsx delete mode 100644 explorer/src/icons/ElipsSVG.tsx delete mode 100644 explorer/src/icons/GatewaysSVG.tsx delete mode 100644 explorer/src/icons/LightSwitchSVG.tsx delete mode 100644 explorer/src/icons/MixnodesSVG.tsx delete mode 100644 explorer/src/icons/MobileDrawerClose.tsx delete mode 100644 explorer/src/icons/NetworksSVG.tsx delete mode 100644 explorer/src/icons/NodemapSVG.tsx delete mode 100644 explorer/src/icons/NymVpn.tsx delete mode 100644 explorer/src/icons/OverviewSVG.tsx delete mode 100644 explorer/src/icons/TokenSVG.tsx delete mode 100644 explorer/src/icons/ValidatorsSVG.tsx delete mode 100644 explorer/src/icons/socials/DiscordIcon.tsx delete mode 100644 explorer/src/icons/socials/GitHubIcon.tsx delete mode 100644 explorer/src/icons/socials/TelegramIcon.tsx delete mode 100644 explorer/src/icons/socials/TwitterIcon.tsx delete mode 100644 explorer/src/index.html delete mode 100644 explorer/src/index.tsx delete mode 100644 explorer/src/pages/404/index.tsx delete mode 100644 explorer/src/pages/Delegations/index.tsx delete mode 100644 explorer/src/pages/GatewayDetail/index.tsx delete mode 100644 explorer/src/pages/Gateways/index.tsx delete mode 100644 explorer/src/pages/MixnodeDetail/index.tsx delete mode 100644 explorer/src/pages/Mixnodes/index.tsx delete mode 100644 explorer/src/pages/MixnodesMap/index.tsx delete mode 100644 explorer/src/pages/Overview/index.tsx delete mode 100644 explorer/src/pages/ServiceProviders/index.tsx delete mode 100644 explorer/src/routes/index.tsx delete mode 100644 explorer/src/routes/network-components.tsx delete mode 100644 explorer/src/stories/Introduction.stories.mdx delete mode 100644 explorer/src/styles.css delete mode 100644 explorer/src/tests/Nav.test.tsx delete mode 100644 explorer/src/tests/WorldMap.test.tsx delete mode 100644 explorer/src/tests/todo.test.ts delete mode 100644 explorer/src/theme/index.tsx delete mode 100644 explorer/src/theme/mui-theme.d.ts delete mode 100644 explorer/src/typeDefs/explorer-api.ts delete mode 100644 explorer/src/typeDefs/filters.ts delete mode 100644 explorer/src/typeDefs/network.ts delete mode 100644 explorer/src/typeDefs/tables.ts delete mode 100644 explorer/src/typings/FC.d.ts delete mode 100644 explorer/src/typings/jpeg.d.ts delete mode 100644 explorer/src/typings/json.d.ts delete mode 100644 explorer/src/typings/png.d.ts delete mode 100644 explorer/src/typings/react-identicons.d.ts delete mode 100644 explorer/src/typings/react-tooltip.d.ts delete mode 100644 explorer/src/typings/svg.d.ts delete mode 100644 explorer/src/utils/currency.ts delete mode 100644 explorer/src/utils/index.ts delete mode 100644 explorer/src/x-data-grid/LICENSE delete mode 100644 explorer/src/x-data-grid/README.md delete mode 100644 explorer/src/x-data-grid/package.json delete mode 100644 explorer/tsconfig.json delete mode 100644 explorer/webpack.common.js delete mode 100644 explorer/webpack.config.js delete mode 100644 explorer/webpack.prod.js diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5863df8822..be2ede84c1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -5,7 +5,6 @@ on: paths: - 'clients/**' - 'common/**' - - 'explorer-api/**' - 'gateway/**' - 'integrations/**' - 'nym-api/**' diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index 618304e9ae..07b358c45f 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -19,7 +19,11 @@ jobs: if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }} strategy: fail-fast: false - runs-on: arc-ubuntu-22.04 + matrix: + include: + - os: arc-ubuntu-22.04 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} outputs: release_id: ${{ steps.create-release.outputs.id }} @@ -66,7 +70,6 @@ jobs: with: name: my-artifact path: | - target/release/explorer-api target/release/nym-client target/release/nym-socks5-client target/release/nym-api @@ -82,7 +85,6 @@ jobs: if: github.event_name == 'release' with: files: | - target/release/explorer-api target/release/nym-client target/release/nym-socks5-client target/release/nym-api diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs index aa52baa876..64de45df77 100644 --- a/common/network-defaults/build.rs +++ b/common/network-defaults/build.rs @@ -23,7 +23,6 @@ fn main() { "REWARDING_VALIDATOR_ADDRESS", "NYM_API", "NYXD_WS", - "EXPLORER_API", "NYM_VPN_API", ]; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 2b54d17406..501c46651b 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -31,7 +31,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772 pub const NYXD_URL: &str = "https://rpc.nymtech.net"; pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; -pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging @@ -123,7 +122,6 @@ pub fn export_to_env() { set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API); } @@ -165,6 +163,5 @@ pub fn export_to_env_if_not_set() { set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 8cebe53340..5a3e6ab711 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -35,7 +35,6 @@ pub struct NymNetworkDetails { pub chain_details: ChainDetails, pub endpoints: Vec, pub contracts: NymContracts, - pub explorer_api: Option, pub nym_vpn_api_url: Option, } @@ -65,7 +64,6 @@ impl NymNetworkDetails { }, endpoints: Default::default(), contracts: Default::default(), - explorer_api: Default::default(), nym_vpn_api_url: Default::default(), } } @@ -124,7 +122,6 @@ impl NymNetworkDetails { .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) - .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API)) } @@ -152,7 +149,6 @@ impl NymNetworkDetails { mainnet::COCONUT_DKG_CONTRACT_ADDRESS, ), }, - explorer_api: parse_optional_str(mainnet::EXPLORER_API), nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), } } @@ -193,7 +189,6 @@ impl NymNetworkDetails { set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address); set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address); - set_optional_var(var_names::EXPLORER_API, self.explorer_api); set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url); } @@ -297,12 +292,6 @@ impl NymNetworkDetails { self } - #[must_use] - pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { - self.explorer_api = endpoint.map(Into::into); - self - } - #[must_use] pub fn with_nym_vpn_api_url>(mut self, endpoint: Option) -> Self { self.nym_vpn_api_url = endpoint.map(Into::into); diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index bf92b95799..5bcebe85eb 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; pub const NYXD_WEBSOCKET: &str = "NYXD_WS"; -pub const EXPLORER_API: &str = "EXPLORER_API"; pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; pub const NYM_VPN_API: &str = "NYM_VPN_API"; pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER"; diff --git a/envs/canary.env b/envs/canary.env index 6bdd85494e..f494ad3270 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket diff --git a/envs/mainnet-local-api.env b/envs/mainnet-local-api.env index d6683c937d..f7d1fec6d5 100644 --- a/envs/mainnet-local-api.env +++ b/envs/mainnet-local-api.env @@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" NYM_API="http://127.0.0.1:8000" NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/mainnet.env b/envs/mainnet.env index ad1e2cd88e..444307f836 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090 NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket -EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API=https://nymvpn.com/api/ diff --git a/envs/qa.env b/envs/qa.env index 2a4fddb0a7..1a350109e2 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -18,7 +18,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq683 VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 -EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ NYXD=https://rpc.qa-validator.qa.nymte.ch NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket NYM_API=https://qa-nym-api.qa.nymte.ch/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 68e51fec59..e90257f015 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ diff --git a/explorer-nextjs/.eslintrc.json b/explorer-nextjs/.eslintrc.json deleted file mode 100644 index 957cd1545e..0000000000 --- a/explorer-nextjs/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals"] -} diff --git a/explorer-nextjs/.gitignore b/explorer-nextjs/.gitignore deleted file mode 100644 index fd3dbb571a..0000000000 --- a/explorer-nextjs/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/explorer-nextjs/README.md b/explorer-nextjs/README.md deleted file mode 100644 index c4033664f8..0000000000 --- a/explorer-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/explorer-nextjs/app/App.tsx b/explorer-nextjs/app/App.tsx deleted file mode 100644 index 6763797c84..0000000000 --- a/explorer-nextjs/app/App.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { Navbar } from './components/Nav/Navbar' -import { Providers } from './providers' - -const App = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -export { App } diff --git a/explorer-nextjs/app/account/[id]/page.tsx b/explorer-nextjs/app/account/[id]/page.tsx deleted file mode 100644 index c0c482476c..0000000000 --- a/explorer-nextjs/app/account/[id]/page.tsx +++ /dev/null @@ -1,407 +0,0 @@ -'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[] - >(() => { - return [ - { - id: 'accumulated-rewards-data', - header: 'Accumulated Rewards Data', - columns: [ - { - id: 'node_id', - accessorKey: 'node_id', - header: 'Node ID', - size: 150, - Cell: ({ row }) => ( - {row.original.node_id} - ), - }, - { - id: 'node_still_fully_bonded', - accessorKey: 'node_still_fully_bonded', - header: 'Node still bonded?', - width: 150, - Cell: ({ row }) => ( - <>{row.original.node_still_fully_bonded ? : - theme.palette.warning.main }}> - - Unbonded - } - ) - }, - { - 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 }) => ( - {humanReadableCurrencyToString(row.original.rewards)} - ) - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: account?.accumulated_rewards || [], - enableFullScreenToggle: false, - }) - - return (); -} - -const DelegationHistory = ({account}: { account?: any}) => { - const columns = useMemo< - MRT_ColumnDef[] - >(() => { - 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 (); -} - - -/** - * 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> - - <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 diff --git a/explorer-nextjs/app/api/constants.ts b/explorer-nextjs/app/api/constants.ts deleted file mode 100644 index 515655c184..0000000000 --- a/explorer-nextjs/app/api/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1'; -export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net'; - -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 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 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 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'; -export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer-nextjs/app/api/index.ts b/explorer-nextjs/app/api/index.ts deleted file mode 100644 index ae190de9b9..0000000000 --- a/explorer-nextjs/app/api/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - TEMP_UNSTABLE_NYM_NODES, - NYM_API_NODE_UPTIME, - NYM_API_NODE_PERFORMANCE, - TEMP_UNSTABLE_ACCOUNT, - LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - 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; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(LEGACY_MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - return response.json(); - }; - - 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> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - 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 => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer-nextjs/app/assets/world-110m.json b/explorer-nextjs/app/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer-nextjs/app/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer-nextjs/app/components/ComponentError.tsx b/explorer-nextjs/app/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer-nextjs/app/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/ContentCard.tsx b/explorer-nextjs/app/components/ContentCard.tsx deleted file mode 100644 index c83049e5c7..0000000000 --- a/explorer-nextjs/app/components/ContentCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material' -import React, { ReactEventHandler } from 'react' - -type ContentCardProps = { - title?: React.ReactNode - subtitle?: string - Icon?: React.ReactNode - Action?: React.ReactNode - errorMsg?: string - onClick?: ReactEventHandler -} - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && ( - <CardHeader - title={title || ''} - avatar={Icon} - action={Action} - subheader={subtitle} - /> - )} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -) diff --git a/explorer-nextjs/app/components/CustomColumnHeading.tsx b/explorer-nextjs/app/components/CustomColumnHeading.tsx deleted file mode 100644 index e767db34c1..0000000000 --- a/explorer-nextjs/app/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { Box, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' - -export const CustomColumnHeading: FCWithChildren<{ - headingTitle: string - tooltipInfo?: string -}> = ({ headingTitle, tooltipInfo }) => { - const theme = useTheme() - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx b/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx b/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 9ea2f54b6b..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { Button, IconButton } from '@mui/material' -import { SxProps } from '@mui/system' -import { useIsMobile } from '@/app/hooks' -import { DelegateIcon } from '@/app/icons/DelevateSVG' - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium' - disabled?: boolean - tooltip?: React.ReactNode - sx?: SxProps - onDelegate: () => void -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile() - - const handleOnDelegate = () => { - onDelegate() - } - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ) - } - - return ( - <Button - variant="outlined" - size={size} - disabled={disabled} - onClick={handleOnDelegate} - sx={sx} - > - Delegate - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx b/explorer-nextjs/app/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 5b453e8ad5..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,191 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { Box, SxProps } from '@mui/material' -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField' -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField' -import { CurrencyDenom, DecCoin } from '@nymproject/types' -import { useWalletContext } from '@/app/context/wallet' -import { urls } from '@/app/utils' -import { useDelegationsContext } from '@/app/context/delegations' -import { validateAmount } from '@/app/utils/currency' -import { SimpleModal } from './SimpleModal' -import { ModalListItem } from './ModalListItem' -import { DelegationModalProps } from './DelegationModal' - -const MIN_AMOUNT_TO_DELEGATE = 10 - -type Props = { - mixId: number - identityKey: string - header?: string - buttonText?: string - rewardInterval?: string - estimatedReward?: number - profitMarginPercentage?: string | null - nodeUptimePercentage?: number | null - denom: CurrencyDenom - sx?: SxProps - backdropProps?: object - onClose: () => void - onOk?: (delegationModalProps: DelegationModalProps) => void -} - -export const DelegateModal = ({ - mixId, - identityKey, - onClose, - onOk, - denom, - sx, -}: Props) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ - amount: '10', - denom: 'nym', - }) - const [isValidated, setValidated] = useState<boolean>(false) - const [errorAmount, setErrorAmount] = useState<string | undefined>() - - const { address, balance } = useWalletContext() - const { handleDelegate } = useDelegationsContext() - - const validate = async () => { - let newValidatedValue = true - let errorAmountMessage - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false - errorAmountMessage = 'Please enter a valid amount' - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}` - newValidatedValue = false - } - - if (!amount?.amount.length) { - newValidatedValue = false - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds' - newValidatedValue = false - } - - setErrorAmount(errorAmountMessage) - setValidated(newValidatedValue) - } - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number - delegationAmount: string - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount) - return tx - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }) - try { - if (!address) { - throw new Error('Please connect your wallet') - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }) - - if (!tx) { - throw new Error('Failed to delegate') - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } catch (e) { - console.error('Failed to delegate', e) - onOk({ - status: 'error', - message: (e as Error).message, - }) - } - } - } - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount) - } - - React.useEffect(() => { - validate() - }, [amount, identityKey, mixId]) - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - showCoinMark={false} - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem - label="Account balance" - value={`${balance.data} NYM`} - divider - fontWeight={600} - /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx b/explorer-nextjs/app/components/Delegations/DelegationModal.tsx deleted file mode 100644 index 76c37a9e96..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import { Typography, SxProps, Stack } from '@mui/material' -import { Link } from '@nymproject/react/link/Link' -import { LoadingModal } from './LoadingModal' -import { ConfirmationModal } from './ConfirmationModal' -import { ErrorModal } from './ErrorModal' - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info' - message?: string - transactions?: { - url: string - hash: string - }[] -} - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean - onClose: () => void - sx?: SxProps - backdropProps?: object - children?: React.ReactNode - } -> = ({ - status, - message, - transactions, - open, - onClose, - children, - sx, - backdropProps, -}) => { - if (status === 'loading') - return <LoadingModal sx={sx} backdropProps={backdropProps} /> - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ) - } - - if (status === 'info') { - return ( - <ConfirmationModal - open={open} - title="Connect wallet" - confirmButton="OK" - onConfirm={onClose} - > - <Typography>{message}</Typography> - </ConfirmationModal> - ) - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link - href={transactions[0].url} - target="_blank" - sx={{ ml: 1 }} - text="View on blockchain" - noIcon - /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link - href={url} - target="_blank" - sx={{ ml: 1 }} - text={hash.slice(0, 6)} - key={hash} - noIcon - /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx b/explorer-nextjs/app/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx b/explorer-nextjs/app/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx b/explorer-nextjs/app/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx b/explorer-nextjs/app/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx b/explorer-nextjs/app/components/Delegations/SimpleModal.tsx deleted file mode 100644 index b1909c8c5e..0000000000 --- a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react' -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import ErrorOutline from '@mui/icons-material/ErrorOutline' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}) - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void - label?: string - fullWidth?: boolean - sx?: SxProps -}) => ( - <Button - disableFocusRipple - size="large" - fullWidth={fullWidth} - variant="outlined" - onClick={onBack} - sx={sx} - > - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -) - -export const SimpleModal: FCWithChildren<{ - open: boolean - hideCloseIcon?: boolean - displayErrorIcon?: boolean - displayInfoIcon?: boolean - headerStyles?: SxProps - subHeaderStyles?: SxProps - buttonFullWidth?: boolean - onClose?: () => void - onOk?: () => Promise<void> - onBack?: () => void - header: string | React.ReactNode - subHeader?: string - okLabel: string - backLabel?: string - backButtonFullWidth?: boolean - okDisabled?: boolean - sx?: SxProps - children?: React.ReactNode -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile() - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack - direction="row" - justifyContent="space-between" - alignItems="center" - > - {typeof header === 'string' ? ( - <Typography - fontSize={20} - fontWeight={600} - sx={{ color: 'text.primary', ...headerStyles }} - > - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography - mt={subHeader ? 0.5 : 0} - mb={3} - fontSize={12} - color={(theme) => theme.palette.text.secondary} - > - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: 2, - mt: 2, - width: buttonFullWidth ? '100%' : null, - }} - > - {onBack && ( - <StyledBackButton - onBack={onBack} - label={backLabel} - fullWidth={backButtonFullWidth} - /> - )} - {onOk && ( - <Button - variant="contained" - fullWidth - size="large" - onClick={onOk} - disabled={okDisabled} - > - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/index.ts b/explorer-nextjs/app/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer-nextjs/app/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer-nextjs/app/components/Delegations/styles.ts b/explorer-nextjs/app/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer-nextjs/app/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer-nextjs/app/components/DetailTable.tsx b/explorer-nextjs/app/components/DetailTable.tsx deleted file mode 100644 index b068a5c852..0000000000 --- a/explorer-nextjs/app/components/DetailTable.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from 'react' -import { - Link, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableCellProps, -} from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Box } from '@mui/system' -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 - title: string - headerAlign?: TableCellProps['align'] - width?: string | number - tooltipInfo?: string -} - -export interface UniversalTableProps<T = any> { - tableName: string - columnsData: ColumnsType[] - rows: T[] -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ) - } - - if (field === 'bond') { - return unymToNym(val, 6) - } - - if (field === 'owner') { - return ( - <Link - underline="none" - color="inherit" - target="_blank" - href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`} - > - {val} - </Link> - ) - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} /> - } - - return val -} - -export const DetailTable: FCWithChildren<{ - tableName: string - columnsData: ColumnsType[] - rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[] -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme() - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, width, tooltipInfo }) => ( - <TableCell - key={field} - sx={{ fontSize: 14, fontWeight: 600, width }} - > - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Box sx={{ display: 'flex', alignItems: 'center' }}> - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={ - theme.palette.nym.networkExplorer.tooltip.color - } - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - </Box> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues( - eachRow[columnsData[index].field], - columnsData[index].field - )} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/Filters/Filters.tsx b/explorer-nextjs/app/components/Filters/Filters.tsx deleted file mode 100644 index 3a871a5fb4..0000000000 --- a/explorer-nextjs/app/components/Filters/Filters.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client' - -import React, { useState, useEffect, useRef, useCallback } from 'react' -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material' -import { useParams } from 'next/navigation' -import { useMainContext } from '@/app/context/main' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey, TFilterItem, TFilters } from '@/app/typeDefs/filters' -import { Api } from '@/app/api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { formatOnSave, generateFilterSchema } from './filterSchema' -import FiltersButton from './FiltersButton' - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => - onChange(id, newValue as number[]) - } - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => - val === 100 && id === 'stakeSaturation' ? '>100' : val - } - /> - </Box> -) - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext() - const { status } = useParams<{ - status: 'active' | 'standby' | 'inactive' | 'all' - }>() - const isMobile = useIsMobile() - - const [showFilters, setShowFilters] = useState(false) - const [isFiltered, setIsFiltered] = useState(false) - const [filters, setFilters] = React.useState<TFilters>() - const [upperSaturationValue, setUpperSaturationValue] = - React.useState<number>(100) - - const baseFilters = useRef<TFilters>() - const prevFilters = useRef<TFilters>() - - const handleToggleShowFilters = () => setShowFilters(!showFilters) - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes() - if (allMixnodes) { - setUpperSaturationValue( - Math.round( - Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1 - ) - ) - const initFilters = generateFilterSchema() - baseFilters.current = initFilters - prevFilters.current = initFilters - setFilters(initFilters) - } - }, []) - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue) - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - } - return undefined - }) - } - - const handleOnSave = async () => { - setShowFilters(false) - await filterMixnodes(formatOnSave(filters!), status) - setIsFiltered(true) - prevFilters.current = filters - } - - const handleOnCancel = () => { - setShowFilters(false) - setFilters(prevFilters.current) - } - - const resetFilters = () => { - setFilters(baseFilters.current) - setIsFiltered(false) - prevFilters.current = baseFilters.current - } - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(MixnodeStatusWithAll[status])) - resetFilters() - } - - useEffect(() => { - initialiseFilters() - }, [initialiseFilters]) - - useEffect(() => { - resetFilters() - }, [status]) - - if (!filters) return null - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog - open={showFilters} - onClose={handleToggleShowFilters} - maxWidth="md" - fullWidth - > - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ) -} diff --git a/explorer-nextjs/app/components/Filters/FiltersButton.tsx b/explorer-nextjs/app/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer-nextjs/app/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer-nextjs/app/components/Filters/filterSchema.ts b/explorer-nextjs/app/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer-nextjs/app/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer-nextjs/app/components/Footer.tsx b/explorer-nextjs/app/components/Footer.tsx deleted file mode 100644 index 3e69bd3c42..0000000000 --- a/explorer-nextjs/app/components/Footer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import Box from '@mui/material/Box' -import MuiLink from '@mui/material/Link' -import Typography from '@mui/material/Typography' -import { useIsMobile } from '../hooks/useIsMobile' -import { NymVpnIcon } from '../icons/NymVpn' -import { Socials } from './Socials' -import Link from 'next/link' - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile() - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <Box marginRight={1}> - <Link href="http://nymvpn.com" target="_blank"> - <NymVpnIcon /> - </Link> - </Box> - - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Gateways/Gateways.ts b/explorer-nextjs/app/components/Gateways/Gateways.ts deleted file mode 100644 index f4dbf2743c..0000000000 --- a/explorer-nextjs/app/components/Gateways/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api'; -import { toPercentInteger } from '@/app/utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; -// node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', -// node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - 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), - }; -} diff --git a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx b/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index 10f230e26a..0000000000 --- a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { FormControl, MenuItem, Select } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions - handleChange: (option: VersionSelectOptions) => void -}) => { - const isMobile = useIsMobile() - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem - value={VersionSelectOptions.latestVersion} - data-testid="show-gateway-latest-version" - > - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem - value={VersionSelectOptions.olderVersions} - data-testid="show-gateway-old-versions" - > - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem - value={VersionSelectOptions.all} - data-testid="show-gateway-all-versions" - > - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ) -} diff --git a/explorer-nextjs/app/components/Icons.ts b/explorer-nextjs/app/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer-nextjs/app/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx b/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 58ea4c97b2..0000000000 --- a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import * as React from 'react' -import { Alert, Box, CircularProgress, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -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 TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import { ExpandMore } from '@mui/icons-material' -import { currencyToString } from '@/app/utils/currency' -import { useMixnodeContext } from '@/app/context/mixnode' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext() - const [showDelegations, toggleShowDelegations] = - React.useState<boolean>(false) - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }) - const theme = useTheme() - const isMobile = useIsMobile() - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }) - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }) - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount) - const delegationsSum = Number(mixNode.data.total_delegation.amount) - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }) - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }) - } - }, [mixNode]) - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations) - } - } - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount) - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount) - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount - return ((num * 100) / rawTotalBondsAmount).toFixed(1) - } - return 0 - } - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert> - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert> - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && ( - <ExpandMore /> - )} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left"> - {currencyToString({ amount: amount.toString() })} - </TableCell> - <TableCell align="left"> - {calcBondPercentage(amount)}% - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx b/explorer-nextjs/app/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 94a24f338d..0000000000 --- a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react' -import { Box, Button, Grid, Typography, useTheme } from '@mui/material' -import Identicon from 'react-identicons' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeDescriptionResponse } from '@/app/typeDefs/explorer-api' -import { getMixNodeStatusText, MixNodeStatus } from './Status' -import { MixnodeRowType } from '.' - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType - mixnodeDescription: MixNodeDescriptionResponse -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ - mixNodeRow, - mixnodeDescription, -}) => { - const theme = useTheme() - const palette = [theme.palette.text.primary] - const isMobile = useIsMobile() - const statusText = React.useMemo( - () => getMixNodeStatusText(mixNodeRow.status), - [mixNodeRow.status] - ) - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box - display="flex" - flexDirection={isMobile ? 'column' : 'row'} - width="100%" - > - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon - size={43} - string={mixNodeRow.identity_key} - palette={palette} - /> - </Box> - <Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}> - <Typography fontSize={21}>{mixnodeDescription.name}</Typography> - <Typography> - {(mixnodeDescription.description || '').slice(0, 1000)} - </Typography> - <Button - component="a" - variant="text" - sx={{ - mt: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <Grid - item - xs={12} - md={6} - display="flex" - justifyContent={isMobile ? 'start' : 'end'} - mt={isMobile ? 3 : undefined} - > - <Box display="flex" flexDirection="column"> - <Typography - fontWeight="600" - alignSelf={isMobile ? 'start' : 'self-end'} - > - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts b/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - width: '15%', - tooltipInfo: - 'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.', - }, - { - field: 'estimatedOperatorReward', - title: 'Estimated Operator Reward', - width: '15%', - tooltipInfo: - "Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.", - }, - { - field: 'selectionChance', - title: 'Active Set Probability', - width: '12.5%', - tooltipInfo: - 'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.', - }, - { - field: 'profitMargin', - title: 'Profit Margin', - width: '12.5%', - tooltipInfo: - 'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.', - }, - { - field: 'operatingCost', - title: 'Operating Cost', - width: '10%', - tooltipInfo: - 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', - }, - { - field: 'nodePerformance', - title: 'Routing Score', - width: '10%', - 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.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts b/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index 62b27cf229..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '@/app/utils/currency'; -import { useMixnodeContext } from '@/app/context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '@/app/typeDefs/explorer-api'; -import { toPercentIntegerString } from '@/app/utils'; -import { EconomicsInfoRowWithIndex } from './types'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - const avgUptime = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.last_24h) - : '-'; - const nodePerformance = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.most_recent) - : '-'; - - const opCost = mixNode?.data?.operating_cost; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index e497a72edd..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { EconomicsProgress } from './EconomicsProgress' - -export const StakeSaturationProgressBar = ({ - value, - threshold, -}: { - value: number - threshold: number -}) => { - const isTablet = useIsMobile('lg') - const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit' - const textColor = - percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee' - - return ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - flexDirection: isTablet ? 'column' : 'row', - }} - id="field" - color={percentageColor} - > - <Typography - sx={{ - mr: isTablet ? 0 : 1, - mb: isTablet ? 1 : 0, - fontWeight: '600', - fontSize: '12px', - color: textColor, - }} - id="stake-saturation-progress-bar" - > - {value}% - </Typography> - <EconomicsProgress - value={value} - threshold={threshold} - color={percentageColor} - /> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx b/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index a70a013a24..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react' -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { Box } from '@mui/system' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types' -import { UniversalTableProps } from '@/app/components/DetailTable' -import { textColour } from '@/app/utils' - -const formatCellValues = (value: EconomicsRowsType, field: string) => ( - <Box sx={{ display: 'flex', alignItems: 'center' }} id="field"> - <Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}> - {value.value} - </Typography> - </Box> -) - -export const DelegatorsInfoTable: FCWithChildren< - UniversalTableProps<EconomicsInfoRowWithIndex> -> = ({ tableName, columnsData, rows }) => { - const theme = useTheme() - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell - key={field} - sx={{ fontSize: 14, fontWeight: 600, width }} - > - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={ - theme.palette.nym.networkExplorer.tooltip.color - } - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index] - const value: EconomicsRowsType = (eachRow as any)[field] - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ) - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/index.ts b/explorer-nextjs/app/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/types.ts b/explorer-nextjs/app/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer-nextjs/app/components/MixNodes/Status.tsx b/explorer-nextjs/app/components/MixNodes/Status.tsx deleted file mode 100644 index bc8f1fe660..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Status.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react' -import { Typography } from '@mui/material' -import { getMixNodeIcon } from '@/app/components/Icons' -import { MixnodeStatus } from '@/app/typeDefs/explorer-api' -import { useGetMixNodeStatusColor } from '@/app/hooks/useGetMixnodeStatusColor' - -interface MixNodeStatusProps { - status: MixnodeStatus -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active' - case MixnodeStatus.standby: - return 'on standby' - default: - return 'inactive' - } -} - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ - status, -}) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]) - const color = useGetMixNodeStatusColor(status) - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx b/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index dc505869ed..0000000000 --- a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react' -import { MenuItem } from '@mui/material' -import Select from '@mui/material/Select' -import { SelectChangeEvent } from '@mui/material/Select/SelectInput' -import { SxProps } from '@mui/system' -import { - MixnodeStatus, - MixnodeStatusWithAll, -} from '@/app/typeDefs/explorer-api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeStatus } from './Status' - -// TODO: replace with i18n -const ALL_NODES = 'All nodes' - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll - sx?: SxProps - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void -} - -export const MixNodeStatusDropdown: FCWithChildren< - MixNodeStatusDropdownProps -> = ({ status, onSelectionChanged, sx }) => { - const isMobile = useIsMobile() - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>( - status || MixnodeStatusWithAll.all - ) - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll) - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll) - } - }, - [onSelectionChanged] - ) - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case 'active': - case 'standby': - case 'inactive': - return <MixNodeStatus status={value as unknown as MixnodeStatus} /> - default: - return ALL_NODES - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem - value={MixnodeStatus.active} - data-testid="mixnodeStatusSelectOption_active" - > - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.standby} - data-testid="mixnodeStatusSelectOption_standby" - > - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.inactive} - data-testid="mixnodeStatusSelectOption_inactive" - > - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={'all'} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/index.ts b/explorer-nextjs/app/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer-nextjs/app/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer-nextjs/app/components/MixNodes/mappings.ts b/explorer-nextjs/app/components/MixNodes/mappings.ts deleted file mode 100644 index df8558d39b..0000000000 --- a/explorer-nextjs/app/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '@/app/utils'; -import { unymToNym } from '@/app/utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer-nextjs/app/components/Nav/DesktopNav.tsx b/explorer-nextjs/app/components/Nav/DesktopNav.tsx deleted file mode 100644 index e1faab46f1..0000000000 --- a/explorer-nextjs/app/components/Nav/DesktopNav.tsx +++ /dev/null @@ -1,379 +0,0 @@ -'use client' - -import * as React from 'react' -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material' -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles' -import { Link as MuiLink } from '@mui/material' -import Button from '@mui/material/Button' -import Box from '@mui/material/Box' -import ListItem from '@mui/material/ListItem' -import MuiDrawer from '@mui/material/Drawer' -import AppBar from '@mui/material/AppBar' -import Toolbar from '@mui/material/Toolbar' -import Typography from '@mui/material/Typography' -import List from '@mui/material/List' -import IconButton from '@mui/material/IconButton' -import ListItemButton from '@mui/material/ListItemButton' -import ListItemIcon from '@mui/material/ListItemIcon' -import ListItemText from '@mui/material/ListItemText' -import { NYM_WEBSITE } from '@/app/api/constants' -import { useMainContext } from '@/app/context/main' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { NavOptionType, originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -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 - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}) - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}) - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})) - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})) - -type ExpandableButtonType = { - title: string - url: string - isActive?: boolean - Icon?: React.ReactNode - nested?: NavOptionType[] - isChild?: boolean - isMobile: boolean - drawIsTempOpen: boolean - drawIsFixed: boolean - isExternalLink?: boolean - openDrawer: () => void - closeDrawer?: () => void - fixDrawerClose?: () => void -} - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - title, - url, - drawIsTempOpen, - drawIsFixed, - Icon, - nested, - isMobile, - isChild, - isExternalLink, - openDrawer, - closeDrawer, - fixDrawerClose, -}) => { - const { palette } = useTheme() - const pathname = usePathname() - const router = useRouter() - - const handleClick = () => { - if (title === 'Network Components') { - return undefined - } - - if (isExternalLink) { - window.open(url, '_blank') - - return undefined - } - - if (!isExternalLink) { - router.push(url, {}) - } - - if (closeDrawer) { - closeDrawer() - } - } - const selectedStyle = { - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - } - - return ( - <> - <ListItem - disablePadding - disableGutters - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(pathname === url - ? selectedStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={() => handleClick()} - sx={{ - pt: 2, - pb: 2, - background: isChild - ? palette.nym.networkExplorer.nav.selected.nested - : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - /> - </ListItemButton> - </ListItem> - {nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - isExternalLink={each.isExternal} - /> - ))} - </> - ) -} - -export const Nav: FCWithChildren = ({ children }) => { - const { environment } = useMainContext() - const [drawerIsOpen, setDrawerToOpen] = React.useState(false) - const [fixedOpen, setFixedOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const theme = useTheme() - - const explorerName = environment - ? `${environment} Explorer` - : 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - - const fixDrawerOpen = () => { - setFixedOpen(true) - setDrawerToOpen(true) - } - - const fixDrawerClose = () => { - setFixedOpen(false) - setDrawerToOpen(false) - } - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true) - } - } - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false) - } - } - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - {/* <NymLogo /> */} - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink - href="/" - underline="none" - color="inherit" - textTransform="capitalize" - > - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - borderRadius: 2, - textTransform: 'none', - width: 150, - ml: 4, - fontSize: 14, - fontWeight: 600, - }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box> - <SearchToolbar/> - </Box> - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={true} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - display: 'none', - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List - sx={{ pb: 0 }} - onMouseEnter={tempDrawerOpen} - onMouseLeave={tempDrawerClose} - > - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box - style={{ width: `calc(100% - ${drawerWidth}px` }} - sx={{ py: 5, px: 6, mt: 7 }} - > - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/MobileNav.tsx b/explorer-nextjs/app/components/Nav/MobileNav.tsx deleted file mode 100644 index 3d1cfdcbf9..0000000000 --- a/explorer-nextjs/app/components/Nav/MobileNav.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client' - -import * as React from 'react' -import { useTheme } from '@mui/material/styles' -import { - AppBar, - Box, - Drawer, - IconButton, - List, - ListItem, - ListItemButton, - ListItemIcon, - Toolbar, -} from '@mui/material' -import { Menu } from '@mui/icons-material' -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { Footer } from '../Footer' -import { ExpandableButton } from './DesktopNav' -import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet' -import { NetworkTitle } from '../NetworkTitle' -import { originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -import {SearchToolbar} from "@/app/components/Nav/Search"; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme() - const [drawerOpen, setDrawerOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const isSmallMobile = useIsMobile(400) - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen) - } - - const openDrawer = () => { - setDrawerOpen(true) - } - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner - open={openMaintenance} - onClick={() => setOpenMaintenance(false)} - /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <Box sx={{ - alignItems: 'center', - display: 'flex', - }}> - <Box mr={0.5}> - <SearchToolbar/> - </Box> - <ConnectKeplrWallet /> - </Box> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={false} - drawIsFixed={false} - Icon={props.Icon} - nested={props.nested} - closeDrawer={toggleDrawer} - isMobile - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/Navbar.tsx b/explorer-nextjs/app/components/Nav/Navbar.tsx deleted file mode 100644 index 15bcd47566..0000000000 --- a/explorer-nextjs/app/components/Nav/Navbar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client' - -import React from 'react' -import { useIsMobile } from '@/app/hooks' -import { MobileNav } from './MobileNav' -import { Nav } from './DesktopNav' - -const Navbar = ({ children }: { children: React.ReactNode }) => { - const isMobile = useIsMobile() - - if (isMobile) { - return <MobileNav>{children}</MobileNav> - } - - return <Nav>{children}</Nav> -} - -export { Navbar } diff --git a/explorer-nextjs/app/components/Nav/Search.tsx b/explorer-nextjs/app/components/Nav/Search.tsx deleted file mode 100644 index 888125b84a..0000000000 --- a/explorer-nextjs/app/components/Nav/Search.tsx +++ /dev/null @@ -1,76 +0,0 @@ -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> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/components/NetworkTitle.tsx b/explorer-nextjs/app/components/NetworkTitle.tsx deleted file mode 100644 index 0c450007da..0000000000 --- a/explorer-nextjs/app/components/NetworkTitle.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import { Button, Typography, Link as MuiLink } from '@mui/material' -import { useMainContext } from '@/app/context/main' - -type NetworkTitleProps = { - showToggleNetwork?: boolean -} - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext() - - const explorerName = - `${ - environment && environment.charAt(0).toUpperCase() + environment.slice(1) - } Explorer` || 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink href="/" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - textTransform: 'none', - width: 114, - fontSize: '12px', - fontWeight: 600, - ml: 1, - }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ) -} - -export { NetworkTitle } diff --git a/explorer-nextjs/app/components/ReleaseAlert.tsx b/explorer-nextjs/app/components/ReleaseAlert.tsx deleted file mode 100644 index 5d974420f9..0000000000 --- a/explorer-nextjs/app/components/ReleaseAlert.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Alert, Box, Typography } from '@mui/material'; - -export const ReleaseAlert = () => ( - <Alert severity="warning" sx={{ mb: 3, fontSize: 'medium', width: '100%' }}> - <Box> - <Typography>You are now viewing the legacy Nym mixnet explorer. Explorer 2.0 is coming soon.</Typography> - </Box> - </Alert> -); diff --git a/explorer-nextjs/app/components/Socials.tsx b/explorer-nextjs/app/components/Socials.tsx deleted file mode 100644 index 4a4856c8f2..0000000000 --- a/explorer-nextjs/app/components/Socials.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' -import { Box, IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { TelegramIcon } from '../icons/socials/TelegramIcon' -import { GitHubIcon } from '../icons/socials/GitHubIcon' -import { TwitterIcon } from '../icons/socials/TwitterIcon' -import { DiscordIcon } from '../icons/socials/DiscordIcon' - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ - isFooter = false, -}) => { - const theme = useTheme() - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons - return ( - <Box> - <IconButton - component="a" - href={TELEGRAM_LINK} - target="_blank" - data-testid="telegram" - > - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={DISCORD_LINK} - target="_blank" - data-testid="discord" - > - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={TWITTER_LINK} - target="_blank" - data-testid="twitter" - > - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={GITHUB_LINK} - target="_blank" - data-testid="github" - > - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/StatsCard.tsx b/explorer-nextjs/app/components/StatsCard.tsx deleted file mode 100644 index 2c87998986..0000000000 --- a/explorer-nextjs/app/components/StatsCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react' -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import EastIcon from '@mui/icons-material/East' - -interface StatsCardProps { - icon: React.ReactNode - title: string - count?: string | number - errorMsg?: Error | string - onClick?: () => void - color?: string -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography - ml={3} - mr={0.75} - fontSize="inherit" - fontWeight="inherit" - data-testid={`${title}-amount`} - > - {count === undefined || count === null ? '' : count} - </Typography> - <Typography - mr={1} - fontSize="inherit" - fontWeight="inherit" - data-testid={title} - > - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' - ? errorMsg - : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ) -} diff --git a/explorer-nextjs/app/components/StyledLink.tsx b/explorer-nextjs/app/components/StyledLink.tsx deleted file mode 100644 index 36dd798180..0000000000 --- a/explorer-nextjs/app/components/StyledLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Link as MuiLink, SxProps, Typography } from '@mui/material' -import Link from 'next/link' - -type StyledLinkProps = { - to: string - children: React.ReactNode - target?: React.HTMLAttributeAnchorTarget - dataTestId?: string - color?: string - sx?: SxProps -} - -const StyledLink = ({ - to, - children, - dataTestId, - target, - color, - sx, -}: StyledLinkProps) => ( - <Link - href={to} - target={target} - data-testid={dataTestId} - style={{ textDecoration: 'none' }} - > - <Typography component="a" sx={{ ...sx }} color={color}> - {children} - </Typography> - </Link> -) - -export default StyledLink diff --git a/explorer-nextjs/app/components/Switch.tsx b/explorer-nextjs/app/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer-nextjs/app/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer-nextjs/app/components/TableToolbar.tsx b/explorer-nextjs/app/components/TableToolbar.tsx deleted file mode 100644 index f22b84aca9..0000000000 --- a/explorer-nextjs/app/components/TableToolbar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' - -import React from 'react' -import { Box, SelectChangeEvent } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { Filters } from './Filters/Filters' - -const fieldsHeight = '42.25px' - -type TableToolBarProps = { - childrenBefore?: React.ReactNode - childrenAfter?: React.ReactNode -} - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - childrenBefore, - childrenAfter, -}) => { - const isMobile = useIsMobile() - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: isMobile ? 'column-reverse' : 'row', - alignItems: 'middle', - }} - > - <Box - sx={{ - display: 'flex', - justifyContent: 'space-between', - height: fieldsHeight, - }} - > - {childrenBefore} - </Box> - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {childrenAfter} - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Title.tsx b/explorer-nextjs/app/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer-nextjs/app/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/Tooltip.tsx b/explorer-nextjs/app/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer-nextjs/app/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer-nextjs/app/components/TwoColSmallTable.tsx b/explorer-nextjs/app/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer-nextjs/app/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -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 CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer-nextjs/app/components/Universal-DataGrid.tsx b/explorer-nextjs/app/components/Universal-DataGrid.tsx deleted file mode 100644 index 36e98be9ce..0000000000 --- a/explorer-nextjs/app/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import * as React from 'react' -import { makeStyles } from '@mui/styles' -import { - DataGrid, - GridColDef, - GridEventListener, - useGridApiContext, -} from '@mui/x-data-grid' -import Pagination from '@mui/material/Pagination' -import { LinearProgress } from '@mui/material' -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity' - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}) - -const CustomPagination = () => { - const apiRef = useGridApiContext() - const classes = useStyles() - console.log(apiRef.current.state) - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={apiRef.current.state.pagination.paginationModel.pageSize} - page={apiRef.current.state.pagination.paginationModel.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ) -} - -type DataGridProps = { - columns: GridColDef[] - pagination?: true | undefined - pageSize?: string | undefined - rows: any - loading?: boolean - initialState?: GridInitialStateCommunity - onRowClick?: GridEventListener<'rowClick'> | undefined -} -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress /> - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - slots={{ - pagination: CustomPagination, - }} - columns={columns} - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => - `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => - `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ) -} diff --git a/explorer-nextjs/app/components/UptimeChart.tsx b/explorer-nextjs/app/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer-nextjs/app/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx b/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index 23114f9056..0000000000 --- a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import { Button, IconButton, Stack, CircularProgress } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useWalletContext } from '@/app/context/wallet' -import { WalletAddress, WalletBalance } from '@/app/components/Wallet' - -export const ConnectKeplrWallet = () => { - const { - connectWallet, - disconnectWallet, - isWalletConnected, - isWalletConnecting, - } = useWalletContext() - const isMobile = useIsMobile(1200) - - if (!connectWallet || !disconnectWallet) { - return null - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet() - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ) - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={ - isWalletConnecting && <CircularProgress size={14} color="inherit" /> - } - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx b/explorer-nextjs/app/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 3e251eb2bb..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { ElipsSVG } from '@/app/icons/ElipsSVG' -import { trimAddress } from '@/app/utils' -import { useWalletContext } from '@/app/context/wallet' - -export const WalletAddress = () => { - const { address } = useWalletContext() - - const displayAddress = trimAddress(address, 7) - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx b/explorer-nextjs/app/components/Wallet/WalletBalance.tsx deleted file mode 100644 index 7d1c5979bc..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useWalletContext } from '@/app/context/wallet' -import { useIsMobile } from '@/app/hooks' -import { TokenSVG } from '@/app/icons/TokenSVG' - -export const WalletBalance = () => { - const { balance } = useWalletContext() - const isMobile = useIsMobile(1200) - - const showBalance = !isMobile && balance.status === 'success' - - if (!showBalance) { - return null - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/index.ts b/explorer-nextjs/app/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer-nextjs/app/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer-nextjs/app/components/WorldMap.tsx b/explorer-nextjs/app/components/WorldMap.tsx deleted file mode 100644 index 9b5c329571..0000000000 --- a/explorer-nextjs/app/components/WorldMap.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client' - -import * as React from 'react' -import { scaleLinear } from 'd3-scale' -import { - ComposableMap, - Geographies, - Geography, - Marker, - ZoomableGroup, -} from 'react-simple-maps' -import ReactTooltip from 'react-tooltip' -import { CircularProgress } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api' -import MAP_TOPOJSON from '../assets/world-110m.json' - -type MapProps = { - userLocation?: [number, number] - countryData?: ApiState<CountryDataResponse> - loading: boolean -} - -export const WorldMap: FCWithChildren<MapProps> = ({ - countryData, - userLocation, - loading, -}) => { - const { palette } = useTheme() - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max( - ...Object.values(countryData.data).map((country) => country.nodes) - ) - return scaleLinear<string, string>() - .domain([ - 0, - 1, - heighestNumberOfNodes / 4, - heighestNumberOfNodes / 2, - heighestNumberOfNodes, - ]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]) - } - return () => palette.nym.networkExplorer.map.fills[0] - }, [countryData, palette]) - - const [tooltipContent, setTooltipContent] = React.useState<string | null>( - null - ) - - if (loading) { - return <CircularProgress /> - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3] - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`) - } - }} - onMouseLeave={() => { - setTooltipContent('') - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ) - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ) -} diff --git a/explorer-nextjs/app/components/index.ts b/explorer-nextjs/app/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer-nextjs/app/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer-nextjs/app/context/cosmos-kit.tsx b/explorer-nextjs/app/context/cosmos-kit.tsx deleted file mode 100644 index 987d30aa65..0000000000 --- a/explorer-nextjs/app/context/cosmos-kit.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import React from 'react' -import { ChainProvider } from '@cosmos-kit/react' -import { wallets as keplr } from '@cosmos-kit/keplr-extension' -import { assets, chains } from 'chain-registry' -import { Chain, AssetList } from '@chain-registry/types' -import { VALIDATOR_BASE_URL } from '@/app/api/constants' - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -} - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -} - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx') - - return nyx ? [nymSandbox, nyx] : [nymSandbox] - }, [chains]) - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx') - - return nyx ? [nymSandboxAssets, nyx] : [nymSandboxAssets] - }, [assets]) as AssetList[] - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ) -} - -export default CosmosKitProvider diff --git a/explorer-nextjs/app/context/delegations.tsx b/explorer-nextjs/app/context/delegations.tsx deleted file mode 100644 index 75a099970c..0000000000 --- a/explorer-nextjs/app/context/delegations.tsx +++ /dev/null @@ -1,252 +0,0 @@ -'use client' - -import React, { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from 'react' -import { - Delegation, - PendingEpochEvent, - PendingEpochEventKind, -} from '@nymproject/contract-clients/Mixnet.types' -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' -import { useWalletContext } from './wallet' -import { useMainContext } from './main' - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] } - -export type PendingEvent = ReturnType<typeof getEventsByAddress> - -export type DelegationWithRewards = Delegation & { - rewards: string - identityKey: string - pending: PendingEvent -} - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - } - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - } - } - - return undefined -} - -interface DelegationsState { - delegations?: DelegationWithRewards[] - handleGetDelegations: () => Promise<void> - handleDelegate: ( - mixId: number, - amount: string - ) => Promise<ExecuteResult | undefined> - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined> -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet') - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet') - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const DelegationsProvider = ({ - children, -}: { - children: React.ReactNode -}) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>() - const { address, nymQueryClient, nymClient } = useWalletContext() - const { fetchMixnodes } = useMainContext() - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingEpochEvents({}) - const pendingEvents: PendingEvent[] = [] - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address) - if (event) { - pendingEvents.push(event) - } - }) - - return pendingEvents - } - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingDelegatorReward({ - address, - mixId, - }) - - return response - } - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined) - return undefined - } - - if (!address) { - setDelegations(undefined) - return undefined - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes() - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ - delegator: address, - }) - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => - handleGetDelegationRewards(d.mix_id) - ) - ) - - // Get all pending events - const pendingEvents = await handleGetPendingEvents() - - const delegationsWithRewards: DelegationWithRewards[] = [] - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => - e?.mixId === d.mix_id ? e.kind : undefined - ), - identityKey: - mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || - '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }) - }) - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if ( - e && - !delegationsWithRewards.find( - (d: DelegationWithRewards) => d.mix_id === e.mixId - ) - ) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: - mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node - .identity_key || '', - pending: e, - }) - } - }) - - setDelegations(delegationsWithRewards) - - return undefined - }, [address, nymQueryClient]) - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet') - } - - const amountToDelegate = (Number(amount) * 1000000).toString() - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }] - try { - const tx = await nymClient?.delegateToMixnode( - { mixId }, - fee, - 'Delegation from Nym Explorer', - uNymFunds - ) - - return tx as unknown as ExecuteResult - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode( - { mixId }, - fee, - 'Undelegation from Nym Explorer' - ) - - return tx as unknown as ExecuteResult - } - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations] - ) - - return ( - <DelegationsContext.Provider value={contextValue}> - {children} - </DelegationsContext.Provider> - ) -} - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext) - if (!context) { - throw new Error( - 'useDelegationsContext must be used within a DelegationsProvider' - ) - } - return context -} diff --git a/explorer-nextjs/app/context/gateway.tsx b/explorer-nextjs/app/context/gateway.tsx deleted file mode 100644 index 458a9da907..0000000000 --- a/explorer-nextjs/app/context/gateway.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - GatewayReportResponse, - 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 GatewayState { - uptimeReport?: ApiState<GatewayReportResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const GatewayContext = React.createContext<GatewayState>({}) - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext) - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string - children: JSX.Element -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = - useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById() - clearUptimeHistory() - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) - }, [gatewayIdentityKey]) - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory] - ) - - return ( - <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/hooks.ts b/explorer-nextjs/app/context/hooks.ts deleted file mode 100644 index 55004f89ee..0000000000 --- a/explorer-nextjs/app/context/hooks.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import * as React from 'react'; -import { ApiState } from '@/app/typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer-nextjs/app/context/main.tsx b/explorer-nextjs/app/context/main.tsx deleted file mode 100644 index 0494b87749..0000000000 --- a/explorer-nextjs/app/context/main.tsx +++ /dev/null @@ -1,297 +0,0 @@ -'use client' - -import * as React from 'react' -import { PaletteMode } from '@mui/material' -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey } from '@/app/typeDefs/filters' -import { Api, getEnvironment } from '@/app/api' -import { toPercentIntegerString } from '@/app/utils' -import { NavOptionType, originalNavOptions } from './nav' - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse> - block?: ApiState<BlockResponse> - countryData?: ApiState<CountryDataResponse> - gateways?: ApiState<GatewayResponse> - globalError?: string | undefined - mixnodes?: ApiState<MixNodeResponse> - nodes?: ApiState<any> - mode: PaletteMode - validators?: ApiState<ValidatorsResponse> - environment?: Environment - serviceProviders?: ApiState<DirectoryServiceProvider[]> -} - -interface StateApi { - fetchMixnodes: ( - 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 -} - -type State = StateData & StateApi - -export const MainContext = React.createContext<State>({ - mode: 'dark', - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), - fetchNodes: async () => undefined, - fetchNodeById: async () => undefined, - fetchAccountById: async () => undefined, -}) - -export const useMainContext = (): React.ContextType<typeof MainContext> => - React.useContext<State>(MainContext) - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>() - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark') - - // global / banner error messaging - const [globalError] = React.useState<string>() - - // 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] = - React.useState<ApiState<ValidatorsResponse>>() - const [block, setBlock] = React.useState<ApiState<BlockResponse>>() - const [countryData, setCountryData] = - React.useState<ApiState<CountryDataResponse>>() - const [serviceProviders, setServiceProviders] = - React.useState<ApiState<DirectoryServiceProvider[]>>() - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')) - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary() - setSummaryOverview({ data, isLoading: false }) - } catch (error) { - setSummaryOverview({ - error: - error instanceof Error - ? error - : new Error('Overview summary api fail'), - isLoading: false, - }) - } - } - - const fetchMixnodes = async () => { - let data - setMixnodes((d) => ({ ...d, isLoading: true })) - try { - data = await Api.fetchMixnodes() - setMixnodes({ data, isLoading: false }) - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }) - } - return data - } - - const filterMixnodes = async ( - filters: { [key in EnumFilterKey]: number[] }, - status?: MixnodeStatus - ) => { - setMixnodes((d) => ({ ...d, isLoading: true })) - const mxns = await Api.fetchMixnodes() - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1] - ) - - setMixnodes({ data: filtered, isLoading: false }) - } - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })) - try { - const data = await Api.fetchGateways() - setGateways({ data, isLoading: false }) - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }) - } - } - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators() - setValidators({ data, isLoading: false }) - } catch (error) { - setValidators({ - error: - error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }) - } - } - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock() - setBlock({ data, isLoading: false }) - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }) - } - } - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchCountryData() - setCountryData({ data: res, isLoading: false }) - } catch (error) { - setCountryData({ - error: - error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }) - } - } - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchServiceProviders() - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })) - setServiceProviders({ - data: resWithRoutingScorePercentage, - isLoading: false, - }) - } catch (error) { - setServiceProviders({ - error: - error instanceof Error - ? error - : new Error('Service provider api fail'), - isLoading: false, - }) - } - } - - 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() - } - }, [environment]) - - React.useEffect(() => { - setEnvironment(getEnvironment()) - Promise.all([ - fetchOverviewSummary(), - fetchGateways(), - fetchValidators(), - fetchBlock(), - fetchCountryData(), - ]) - }, []) - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - toggleMode, - fetchMixnodes, - filterMixnodes, - fetchNodes, - fetchNodeById, - fetchAccountById, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - ] - ) - - return <MainContext.Provider value={state}>{children}</MainContext.Provider> -} diff --git a/explorer-nextjs/app/context/mixnode.tsx b/explorer-nextjs/app/context/mixnode.tsx deleted file mode 100644 index 71b48fd045..0000000000 --- a/explorer-nextjs/app/context/mixnode.tsx +++ /dev/null @@ -1,173 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api' -import { Api } from '../api' -import { useApiState } from './hooks' -import { - mixNodeResponseItemToMixnodeRowType, - MixnodeRowType, -} from '../components/MixNodes' - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse> - uniqDelegations?: ApiState<UniqDelegationsResponse> - description?: ApiState<MixNodeDescriptionResponse> - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse> - mixNode?: ApiState<MixNodeResponseItem | undefined> - mixNodeRow?: MixnodeRowType - stats?: ApiState<StatsResponse> - status?: ApiState<StatusResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const MixnodeContext = React.createContext<MixnodeState>({}) - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext) - -interface MixnodeContextProviderProps { - mixId: string - children: React.ReactNode -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren< - MixnodeContextProviderProps -> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState< - MixNodeResponseItem | undefined - >(mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id') - - const [mixNodeRow, setMixnodeRow] = React.useState< - MixnodeRowType | undefined - >() - - const [delegations, fetchDelegations, clearDelegations] = - useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = - useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status' - ) - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats' - ) - - const [description, fetchDescription, clearDescription] = - useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description' - ) - - const [ - economicDynamicsStats, - fetchEconomicDynamicsStats, - clearEconomicDynamicsStats, - ] = useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById() - clearDelegations() - clearUniqDelegations() - clearStatus() - clearStats() - clearDescription() - clearEconomicDynamicsStats() - clearUptimeHistory() - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined) - return - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)) - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]) - }) - }, [mixId]) - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ] - ) - - return ( - <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/nav.tsx b/explorer-nextjs/app/context/nav.tsx deleted file mode 100644 index e41882edbb..0000000000 --- a/explorer-nextjs/app/context/nav.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import * as React from 'react' -import { DelegateIcon } from '@/app/icons/DelevateSVG' -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' - -export type NavOptionType = { - url: string - title: string - Icon?: React.ReactNode - nested?: NavOptionType[] - isExpandedChild?: boolean - isExternal?: boolean -} - -export const originalNavOptions: NavOptionType[] = [ - { - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/nodes', - title: 'Nodes', - }, - { - url: '/network-components/mixnodes', - title: 'Mixnodes (legacy)', - }, - { - url: '/network-components/gateways', - title: 'Gateways (legacy)', - }, - { - url: `${BLOCK_EXPLORER_BASE_URL}/validators`, - title: 'Validators', - isExternal: true, - }, - ], - }, - { - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon sx={{ color: 'white' }} />, - }, -] diff --git a/explorer-nextjs/app/context/node.tsx b/explorer-nextjs/app/context/node.tsx deleted file mode 100644 index 2aa885439c..0000000000 --- a/explorer-nextjs/app/context/node.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'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> - ) -} diff --git a/explorer-nextjs/app/context/wallet.tsx b/explorer-nextjs/app/context/wallet.tsx deleted file mode 100644 index c9acb6b850..0000000000 --- a/explorer-nextjs/app/context/wallet.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client' - -import React, { - createContext, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { useChain } from '@cosmos-kit/react' -import { Wallet } from '@cosmos-kit/core' -import { unymToNym } from '@/app/utils/currency' -import { useNymClient } from '@/app/hooks' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN } from '@/app/api/constants' - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string } - address?: string - isWalletConnected: boolean - isWalletConnecting: boolean - wallet?: Wallet - nymClient?: MixnetClient - nymQueryClient?: MixnetQueryClient - connectWallet: () => Promise<void> - disconnectWallet: () => Promise<void> -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet') - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ - status: 'loading', - data: undefined, - }) - - const { - connect, - disconnect, - wallet, - address, - isWalletConnected, - isWalletConnecting, - getCosmWasmClient, - } = useChain(COSMOS_KIT_USE_CHAIN) - - const { nymClient, nymQueryClient } = useNymClient(address) - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient() - const uNYMBalance = await account.getBalance(walletAddress, 'unym') - const NYMBalance = unymToNym(uNYMBalance.amount) - - return NYMBalance - } - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress) - setBalance({ status: 'success', data: walletBalance }) - } - - useEffect(() => { - if (isWalletConnected && address) { - init(address) - } - }, [address, isWalletConnected]) - - const handleConnectWallet = async () => { - await connect() - } - - const handleDisconnectWallet = async () => { - await disconnect() - setBalance({ status: 'loading', data: undefined }) - } - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - ] - ) - - return ( - <WalletContext.Provider value={contextValue}> - {children} - </WalletContext.Provider> - ) -} - -export const useWalletContext = () => useContext(WalletContext) diff --git a/explorer-nextjs/app/delegations/page.tsx b/explorer-nextjs/app/delegations/page.tsx deleted file mode 100644 index 60b27b3c91..0000000000 --- a/explorer-nextjs/app/delegations/page.tsx +++ /dev/null @@ -1,311 +0,0 @@ -'use client' - -import React, { useCallback, useEffect, useMemo } from 'react' -import { - Alert, - AlertTitle, - Box, - Button, - Card, - Chip, - IconButton, - Tooltip, - Typography, -} from '@mui/material' -import { DelegationModal, DelegationModalProps, Title } from '@/app/components' -import { useWalletContext } from '@/app/context/wallet' -import { unymToNym } from '@/app/utils/currency' -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@/app/context/delegations' -import { urls } from '@/app/utils' -import { useClipboard } from 'use-clipboard-copy' -import { Close } from '@mui/icons-material' -import { useRouter } from 'next/navigation' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const mapToDelegationsRow = ( - delegation: DelegationWithRewards, - index: number -) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}) - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard() - - return ( - <Alert - severity="info" - sx={{ mb: 3, fontSize: 'medium', width: '100%' }} - action={ - <IconButton size="small" onClick={onClose}> - <Close fontSize="small" /> - </IconButton> - } - > - <AlertTitle> Mobile Delegations Beta</AlertTitle> - <Box> - <Typography> - This is a beta release for mobile delegations If you have any feedback - or feature suggestions contact us at support@nymte.ch - <Button - size="small" - onClick={() => copy('support@nymte.ch')} - sx={{ display: 'inline-block' }} - > - Copy - </Button> - </Typography> - </Box> - </Alert> - ) -} - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - const [isLoading, setIsLoading] = React.useState(false) - const [showBanner, setShowBanner] = React.useState(true) - - const { isWalletConnected } = useWalletContext() - const { handleGetDelegations, handleUndelegate, delegations } = - useDelegationsContext() - - const router = useRouter() - - useEffect(() => { - let timeoutId: NodeJS.Timeout - - const fetchDelegations = async () => { - setIsLoading(true) - try { - await handleGetDelegations() - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }) - } finally { - setIsLoading(false) - - timeoutId = setTimeout(() => { - fetchDelegations() - }, 60_000) - } - } - - fetchDelegations() - - return () => { - clearTimeout(timeoutId) - } - }, [handleGetDelegations]) - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending' - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym( - pending.amount.amount - )} NYM` - } - - return undefined - } - - const onUndelegate = useCallback( - async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }) - - try { - const tx = await handleUndelegate(mixId) - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }) - } - } - }, - [handleUndelegate] - ) - - const columns = useMemo< - MRT_ColumnDef<ReturnType<typeof mapToDelegationsRow>>[] - >(() => { - return [ - { - id: 'delegations-data', - header: 'Delegations Data', - columns: [ - { - id: 'identity', - accessorKey: 'identity', - header: 'Identity Key', - width: 400, - }, - { - id: 'mix_id', - accessorKey: 'mix_id', - header: 'Mix ID', - size: 150, - }, - { - id: 'amount', - accessorKey: 'amount', - header: 'Amount', - width: 150, - }, - { - id: 'rewards', - accessorKey: 'rewards', - header: 'Rewards', - width: 150, - enableColumnFilters: false, - }, - { - id: 'undelegate', - accessorKey: 'undelegate', - header: '', - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - Cell: ({ row }) => { - return ( - <Box - sx={{ width: '100%', display: 'flex', justifyContent: 'end' }} - > - {row.original.pending ? ( - <Tooltip - placement="left" - title={getTooltipTitle(row.original.pending)} - onClick={(e) => e.stopPropagation()} - PopperProps={{}} - > - <Chip size="small" label="Pending events" /> - </Tooltip> - ) : ( - <Button - size="small" - variant="outlined" - onClick={(e) => { - e.stopPropagation() - onUndelegate(row.original.mix_id) - }} - > - Undelegate - </Button> - )} - </Box> - ) - }, - }, - ], - }, - ] - }, [onUndelegate]) - - const data = useMemo(() => { - return (delegations || []).map(mapToDelegationsRow) - }, [delegations]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading, - }, - initialState: { - columnPinning: { right: ['undelegate'] }, - }, - }) - - return ( - <Box> - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations() - } - setConfirmationModalProps(undefined) - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && <Banner onClose={() => setShowBanner(false)} />} - <Box display="flex" justifyContent="space-between" alignItems="center"> - <Title text="Your Delegations" /> - <Button - variant="contained" - color="primary" - onClick={() => router.push('/network-components/mixnodes')} - > - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <MaterialReactTable table={table} /> - </Card> - </Box> - ) -} - -const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -) - -export default Delegations diff --git a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx b/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 4dbf9eb16e..0000000000 --- a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react' -import { FallbackProps } from 'react-error-boundary' -import { Alert, AlertTitle, Container } from '@mui/material' -import { NymThemeProvider } from '@nymproject/mui-theme' -import { NymLogo } from '@nymproject/react/logo/NymLogo' - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ - error, -}) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -) diff --git a/explorer-nextjs/app/hooks/index.ts b/explorer-nextjs/app/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer-nextjs/app/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts b/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index 3e1762209f..0000000000 --- a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@/app/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer-nextjs/app/hooks/useIsMobile.ts b/explorer-nextjs/app/hooks/useIsMobile.ts deleted file mode 100644 index 0948d36cbe..0000000000 --- a/explorer-nextjs/app/hooks/useIsMobile.ts +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer-nextjs/app/hooks/useIsMounted.ts b/explorer-nextjs/app/hooks/useIsMounted.ts deleted file mode 100644 index 5a78ae4fec..0000000000 --- a/explorer-nextjs/app/hooks/useIsMounted.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer-nextjs/app/hooks/useNymClient.tsx b/explorer-nextjs/app/hooks/useNymClient.tsx deleted file mode 100644 index 2fa400b130..0000000000 --- a/explorer-nextjs/app/hooks/useNymClient.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useChain } from '@cosmos-kit/react' -import { contracts } from '@nymproject/contract-clients' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@/app/api/constants' - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>() - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>() - - const { getCosmWasmClient, getSigningCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN) - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient() - const cosmWasmClient = await getCosmWasmClient() - - const client = new contracts.Mixnet.MixnetClient( - cosmWasmSigningClient as any, - address, - NYM_MIXNET_CONTRACT - ) - const queryClient = new contracts.Mixnet.MixnetQueryClient( - cosmWasmClient as any, - NYM_MIXNET_CONTRACT - ) - - setNymClient(client) - setNymQueryClient(queryClient) - } - - init() - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]) - - return { nymClient, nymQueryClient } -} diff --git a/explorer-nextjs/app/icons/DelevateSVG.tsx b/explorer-nextjs/app/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer-nextjs/app/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer-nextjs/app/icons/ElipsSVG.tsx b/explorer-nextjs/app/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer-nextjs/app/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer-nextjs/app/icons/GatewaysSVG.tsx b/explorer-nextjs/app/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer-nextjs/app/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/LightSwitchSVG.tsx b/explorer-nextjs/app/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer-nextjs/app/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MixnodesSVG.tsx b/explorer-nextjs/app/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer-nextjs/app/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MobileDrawerClose.tsx b/explorer-nextjs/app/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer-nextjs/app/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer-nextjs/app/icons/NetworksSVG.tsx b/explorer-nextjs/app/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer-nextjs/app/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NodemapSVG.tsx b/explorer-nextjs/app/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer-nextjs/app/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NymVpn.tsx b/explorer-nextjs/app/icons/NymVpn.tsx deleted file mode 100644 index ab9f484d91..0000000000 --- a/explorer-nextjs/app/icons/NymVpn.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' - -interface DiscordIconProps { - size?: { width: number; height: number } -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg - width={size?.width} - height={size?.height} - viewBox="0 0 170 24" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -) diff --git a/explorer-nextjs/app/icons/OverviewSVG.tsx b/explorer-nextjs/app/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer-nextjs/app/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/TokenSVG.tsx b/explorer-nextjs/app/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer-nextjs/app/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/ValidatorsSVG.tsx b/explorer-nextjs/app/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer-nextjs/app/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx b/explorer-nextjs/app/icons/socials/DiscordIcon.tsx deleted file mode 100644 index b81c571c33..0000000000 --- a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface DiscordIconProps { - size?: number | string - color?: string -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx b/explorer-nextjs/app/icons/socials/GitHubIcon.tsx deleted file mode 100644 index 11389e6969..0000000000 --- a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface GitHubIconProps { - size?: number | string - color?: string -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx b/explorer-nextjs/app/icons/socials/TelegramIcon.tsx deleted file mode 100644 index a66020a849..0000000000 --- a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TelegramIconProps { - size?: number | string - color?: string -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx b/explorer-nextjs/app/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 5d94de99a3..0000000000 --- a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TwitterIconProps { - size?: number | string - color?: string -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/layout.tsx b/explorer-nextjs/app/layout.tsx deleted file mode 100644 index a532714d84..0000000000 --- a/explorer-nextjs/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next' -import '@interchain-ui/react/styles' -import { App } from './App' - -export const metadata: Metadata = { - title: 'Nym Network Explorer', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - <html lang="en"> - <body> - <App>{children}</App> - </body> - </html> - ) -} diff --git a/explorer-nextjs/app/loading.tsx b/explorer-nextjs/app/loading.tsx deleted file mode 100644 index bb90466dc5..0000000000 --- a/explorer-nextjs/app/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { LinearProgress, Box } from '@mui/material' - -export default function Loading() { - return ( - <Box sx={{ py: 16 }}> - <LinearProgress /> - </Box> - ) -} diff --git a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx deleted file mode 100644 index 87f779872a..0000000000 --- a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' - -import * as React from 'react' -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' -import { useParams } from 'next/navigation' -import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { - gatewayEnrichedToGridRow, - GatewayEnrichedRowType, -} from '@/app/components/Gateways/Gateways' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { - GatewayContextProvider, - useGatewayContext, -} from '@/app/context/gateway' -import { useMainContext } from '@/app/context/main' -import { Title } from '@/app/components/Title' - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - title: 'Bond', - headerAlign: 'left', - }, - { - field: 'avgUptime', - title: 'Avg. Score', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - 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', - }, -] - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ - selectedGateway, -}: { - selectedGateway: LocatedGateway | undefined -}) => { - const [enrichGateway, setEnrichGateway] = - React.useState<GatewayEnrichedRowType>() - const [status, setStatus] = React.useState<number[] | undefined>() - const { uptimeReport, uptimeStory } = useGatewayContext() - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway( - gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data) - ) - } - }, [uptimeReport, selectedGateway]) - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]) - } - }, [enrichGateway]) - - return ( - <Box component="main"> - <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}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loadingW and not found states - */ -const PageGatewayDetailGuard = () => { - const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>() - const { gateways } = useMainContext() - const { id } = useParams() - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway( - gateways.data.find((g) => g.gateway.identity_key === id) - ) - } - }, [gateways, id]) - - if (gateways?.isLoading) { - return <CircularProgress /> - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageGatewayDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ) -} - -export default PageGatewayDetail diff --git a/explorer-nextjs/app/network-components/gateways/page.tsx b/explorer-nextjs/app/network-components/gateways/page.tsx deleted file mode 100644 index c2660ce387..0000000000 --- a/explorer-nextjs/app/network-components/gateways/page.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { Box, Card, Grid, Stack } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -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 { diff, gte, rcompare } from 'semver' -import { useMainContext } from '@/app/context/main' -import { TableToolbar } from '@/app/components/TableToolbar' -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 { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { splice } from '@/app/utils' -import { - VersionDisplaySelector, - VersionSelectOptions, -} from '@/app/components/Gateways/VersionDisplaySelector' -import StyledLink from '@/app/components/StyledLink' -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() - const [versionFilter, setVersionFilter] = - React.useState<VersionSelectOptions>(VersionSelectOptions.all) - - const theme = useTheme() - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.filter(gatewaySanitize).reduce( - (a: string[], b) => [...a, b.gateway.version], - [] - ) - const [lastestVersion] = versions.sort(rcompare) - return lastestVersion - } - // fallback value - return '2.0.0' - }, [gateways]) - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'patch' || versionDiff === null - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'major' || versionDiff === 'minor' - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions - case VersionSelectOptions.olderVersions: - return filterByOlderVersions - case VersionSelectOptions.all: - return gateways?.data || [] - default: - return [] - } - }, [versionFilter, gateways]) - - const data = useMemo(() => { - return gatewayToGridRow(filteredByVersion || []) - }, [filteredByVersion]) - - const columns = useMemo<MRT_ColumnDef<GatewayRowType>[]>(() => { - return [ - { - id: 'gateway-data', - header: 'Gateways Data', - columns: [ - { - 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.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - dataTestId="identity-link" - color="text.primary" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'version', - header: 'Version', - accessorKey: 'version', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="version" - color="text.primary" - > - {row.original.version} - </StyledLink> - ) - }, - }, - { - id: 'location', - header: 'Location', - accessorKey: 'location', - size: 150, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip - text={row.original.location} - id="gateway-location-text" - > - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {row.original.location} - </Box> - </Tooltip> - </Box> - ) - }, - }, - { - id: 'host', - header: 'IP:Port', - accessorKey: 'host', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="host" - color="text.primary" - > - {row.original.host} - </StyledLink> - ) - }, - }, - { - id: 'owner', - header: 'Owner', - accessorKey: 'owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - target="_blank" - data-testid="owner" - color="text.primary" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ) - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <> - <Box mb={2}> - <Title text="Legacy Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default PageGateways diff --git a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx deleted file mode 100644 index 5e03b5d132..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client' - -import * as React from 'react' -import { - Alert, - AlertTitle, - Box, - CircularProgress, - Grid, - Typography, -} from '@mui/material' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown' -import { - DelegatorsInfoTable, - EconomicsInfoColumns, - EconomicsInfoRows, -} from '@/app/components/MixNodes/Economics' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { WorldMap } from '@/app/components/WorldMap' -import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection' -import { - MixnodeContextProvider, - useMixnodeContext, -} from '@/app/context/mixnode' -import { Title } from '@/app/components/Title' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useParams } from 'next/navigation' - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - width: '12.5%', - }, - { - field: 'stake_saturation', - title: 'Stake Saturation', - width: '12.5%', - tooltipInfo: - 'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.', - }, - { - field: 'self_percentage', - width: '10%', - title: 'Bond %', - tooltipInfo: - "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -] - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState = () => { - const { - mixNode, - mixNodeRow, - description, - stats, - status, - uptimeStory, - uniqDelegations, - } = useMixnodeContext() - const isMobile = useIsMobile() - return ( - <Box component="main"> - <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 && ( - <MixNodeDetailSection - mixNodeRow={mixNodeRow} - mixnodeDescription={description.data} - /> - )} - {mixNodeRow?.blacklisted && ( - <Typography - textAlign={isMobile ? 'left' : 'right'} - fontSize="smaller" - sx={{ color: 'error.main' }} - > - This node is having a poor performance - </Typography> - )} - </Grid> - </Grid> - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Mixnode detail table" - rows={mixNodeRow ? [mixNodeRow] : []} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <DelegatorsInfoTable - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard - title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`} - > - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && ( - <ComponentError text="There was a problem retrieving this nodes stats." /> - )} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || - 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && ( - <ComponentError text="There was a problem retrieving port information" /> - )} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={ - (status?.data?.ports && Object.values(status.data.ports)) || [ - false, - false, - false, - ] - } - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && ( - <ComponentError text="There was a problem retrieving this mixnode location" /> - )} - {mixNode?.data?.location?.latitude && - mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[ - mixNode.data.location.longitude, - mixNode.data.location.latitude, - ]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard = () => { - const { mixNode } = useMixnodeContext() - const { id } = useParams() - - if (mixNode?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageMixnodeDetailWithState /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageMixnodeDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ) -} - -export default PageMixnodeDetail diff --git a/explorer-nextjs/app/network-components/mixnodes/page.tsx b/explorer-nextjs/app/network-components/mixnodes/page.tsx deleted file mode 100644 index 2f2774a7f4..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/page.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client' - -import React, { useCallback, useMemo } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import { - MaterialReactTable, - useMaterialReactTable, - type MRT_ColumnDef, -} from 'material-react-table' -import { Grid, Card, Button, Box, Stack } from '@mui/material' -import { - CustomColumnHeading, - DelegateIconButton, - DelegateModal, - DelegationModal, - DelegationModalProps, - MixNodeStatusDropdown, - MixnodeRowType, - StyledLink, - TableToolbar, - Title, - Tooltip, - mixnodeToGridRow, -} from '@/app/components' -import { DelegationsProvider } from '@/app/context/delegations' -import { useWalletContext } from '@/app/context/wallet' -import { useGetMixNodeStatusColor, useIsMobile } from '@/app/hooks' -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 { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' - -export default function MixnodesPage() { - const isMobile = useIsMobile() - const { isWalletConnected } = useWalletContext() - const { mixnodes, fetchMixnodes } = useMainContext() - const router = useRouter() - - const [itemSelectedForDelegation, setItemSelectedForDelegation] = - React.useState<{ - mixId: number - identityKey: string - }>() - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - - const search = useSearchParams() - const status = search.get('status') as MixnodeStatusWithAll - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)) - }, [status]) - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - router.push( - newStatus && newStatus !== 'all' - ? `/network-components/mixnodes?status=${newStatus}` - : '/network-components/mixnodes' - ) - } - - const handleOnDelegate = useCallback( - ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }) - } else { - setItemSelectedForDelegation({ identityKey, mixId }) - } - }, - [isWalletConnected] - ) - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined) - setConfirmationModalProps(delegationModalProps) - } - - const columns = useMemo<MRT_ColumnDef<MixnodeRowType>[]>(() => { - return [ - { - id: 'mixnode-data', - header: 'Mixnode Data', - columns: [ - { - id: 'delegate', - accessorKey: 'delegate', - size: isMobile ? 50 : 150, - header: '', - grow: false, - Cell: ({ row }) => ( - <DelegateIconButton - size="small" - onDelegate={() => - handleOnDelegate({ - identityKey: row.original.identity_key, - mixId: row.original.mix_id, - }) - } - /> - ), - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - }, - { - 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.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - dataTestId="identity-link" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'bond', - header: 'Stake', - accessorKey: 'bond', - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ amount: row.original.bond.toString() })} - </StyledLink> - ), - }, - { - id: 'stake_saturation', - header: 'Stake Saturation', - accessorKey: 'stake_saturation', - size: 225, - Header() { - return ( - <CustomColumnHeading - headingTitle="Stake Saturation" - tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set." - /> - ) - }, - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.stake_saturation} %`}</StyledLink> - ), - }, - { - id: 'pledge_amount', - header: 'Bond', - accessorKey: 'pledge_amount', - size: 185, - Header: () => ( - <CustomColumnHeading - headingTitle="Bond" - tooltipInfo="Node operator's share of stake." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ - amount: row.original.pledge_amount.toString(), - })} - </StyledLink> - ), - }, - { - id: 'profit_percentage', - accessorKey: 'profit_percentage', - header: 'Profit Margin', - size: 145, - Header: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.profit_percentage}%`}</StyledLink> - ), - }, - { - id: 'operating_cost', - accessorKey: 'operating_cost', - size: 220, - header: 'Operating Cost', - disableColumnMenu: true, - Header: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.operating_cost} NYM`}</StyledLink> - ), - }, - { - id: 'owner', - accessorKey: 'owner', - size: 150, - header: 'Owner', - Header: () => <CustomColumnHeading headingTitle="Owner" />, - Cell: ({ row }) => ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - color={useGetMixNodeStatusColor(row.original.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ), - }, - { - id: 'location', - accessorKey: 'location', - header: 'Location', - maxSize: 150, - Header: () => <CustomColumnHeading headingTitle="Location" />, - Cell: ({ row }) => ( - <Tooltip text={row.original.location} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(row.original.status), - }} - > - {row.original.location} - </Box> - </Tooltip> - ), - }, - { - id: 'host', - accessorKey: 'host', - header: 'Host', - size: 130, - Header: () => <CustomColumnHeading headingTitle="Host" />, - Cell: ({ row }) => ( - <StyledLink - color={useGetMixNodeStatusColor(row.original.status)} - to={`/network-components/mixnodes/${row.original.mix_id}`} - > - {row.original.host} - </StyledLink> - ), - }, - ], - }, - ] - }, [handleOnDelegate, isMobile]) - - const data = useMemo(() => { - return mixnodeToGridRow(mixnodes?.data) - }, [mixnodes?.data]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading: mixnodes?.isLoading, - }, - layoutMode: 'grid-no-grow', - initialState: { - columnPinning: { left: ['delegate'] }, - }, - }) - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Legacy Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown - sx={{ mr: 2 }} - status={status} - onSelectionChanged={handleMixnodeStatusChanged} - /> - } - childrenAfter={ - isWalletConnected && ( - <Button - fullWidth - size="large" - variant="outlined" - onClick={() => router.push('/delegations')} - > - Delegations - </Button> - ) - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined) - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => - handleNewDelegation(delegationModalProps) - } - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined) - if (confirmationModalProps.status === 'success') { - router.push('/delegations') - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ) -} diff --git a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx deleted file mode 100644 index fac36689f2..0000000000 --- a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx +++ /dev/null @@ -1,11 +0,0 @@ -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" />} - </> -) \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx deleted file mode 100644 index a1b754ccce..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx deleted file mode 100644 index 95a172d21e..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"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 ( - { - selectedNymNode.rewarding_details - .unique_delegations - }{" "} - delegates) - </VestingDelegationWarning> - ) : ( - <> - Delegates ( - { - 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, fetchNodeById]); - - 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> - ); - } - - if (!selectedNode) { - return ( - <Alert severity="warning"> - <AlertTitle>Legacy Node</AlertTitle> - Unable to load details for the selected Nym node. - </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; diff --git a/explorer-nextjs/app/network-components/nodes/page.tsx b/explorer-nextjs/app/network-components/nodes/page.tsx deleted file mode 100644 index 75e5b97fdd..0000000000 --- a/explorer-nextjs/app/network-components/nodes/page.tsx +++ /dev/null @@ -1,254 +0,0 @@ -'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())} {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 diff --git a/explorer-nextjs/app/nodemap/page.tsx b/explorer-nextjs/app/nodemap/page.tsx deleted file mode 100644 index 674ae57a5e..0000000000 --- a/explorer-nextjs/app/nodemap/page.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { - Alert, - Box, - CircularProgress, - Grid, - SelectChangeEvent, - Typography, -} from '@mui/material' -import { ContentCard } from '@/app/components/ContentCard' -import { TableToolbar } from '@/app/components/TableToolbar' -import { Title } from '@/app/components/Title' -import { WorldMap } from '@/app/components/WorldMap' -import { useMainContext } from '@/app/context/main' -import { CountryDataRowType, countryDataToGridRow } from '@/app/utils' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const PageMixnodesMap = () => { - const { countryData } = useMainContext() - - const data = useMemo(() => { - return countryDataToGridRow(Object.values(countryData?.data || {})) - }, [countryData]) - - const columns = useMemo<MRT_ColumnDef<CountryDataRowType>[]>(() => { - return [ - { - id: 'delegations-data', - header: 'Global Mixnodes Data', - columns: [ - { - id: 'country-name', - header: 'Location', - accessorKey: 'countryName', - }, - { - id: 'nodes', - header: 'Nodes', - accessorKey: 'nodes', - }, - { - id: 'percentage', - header: 'Percentage', - accessorKey: 'percentage', - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar /> - <MaterialReactTable table={table} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageMixnodesMap diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx deleted file mode 100644 index 2c6f9f781c..0000000000 --- a/explorer-nextjs/app/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client' - -import React, { useEffect } from 'react' -import { Box, Grid, Link, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import OpenInNewIcon from '@mui/icons-material/OpenInNew' -import { PeopleAlt } from '@mui/icons-material' -import { Title } from '@/app/components/Title' -import { StatsCard } from '@/app/components/StatsCard' -import { MixnodesSVG } from '@/app/icons/MixnodesSVG' -import { Icons } from '@/app/components/Icons' -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 { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' -import { formatNumber } from '@/app/utils' -import { useMainContext } from './context/main' -import { useRouter } from 'next/navigation' - -const PageOverview = () => { - const theme = useTheme() - const router = useRouter() - - const { - summaryOverview, - gateways, - validators, - block, - countryData, - serviceProviders, - } = useMainContext() - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Nodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.nymnodes?.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Mixnodes" - count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - 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(`${BLOCK_EXPLORER_BASE_URL}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BLOCK_EXPLORER_BASE_URL}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon - fontWeight="inherit" - fontSize="inherit" - sx={{ ml: 0.5 }} - /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageOverview diff --git a/explorer-nextjs/app/providers/index.tsx b/explorer-nextjs/app/providers/index.tsx deleted file mode 100644 index 346d2fc88e..0000000000 --- a/explorer-nextjs/app/providers/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import CosmosKitProvider from '@/app/context/cosmos-kit' -import { WalletProvider } from '@/app/context/wallet' -import { NetworkExplorerThemeProvider } from '@/app/theme' -import { MainContextProvider } from '@/app/context/main' - -const Providers = ({ children }: { children: React.ReactNode }) => { - return ( - <MainContextProvider> - <NetworkExplorerThemeProvider> - <CosmosKitProvider> - <WalletProvider>{children}</WalletProvider> - </CosmosKitProvider> - </NetworkExplorerThemeProvider> - </MainContextProvider> - ) -} - -export { Providers } diff --git a/explorer-nextjs/app/theme/index.tsx b/explorer-nextjs/app/theme/index.tsx deleted file mode 100644 index fd072cfc64..0000000000 --- a/explorer-nextjs/app/theme/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import * as React from 'react' -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme' -import { useMainContext } from '../context/main' - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext() - - return ( - <NymNetworkExplorerThemeProvider mode={mode}> - {children} - </NymNetworkExplorerThemeProvider> - ) -} diff --git a/explorer-nextjs/app/theme/mui-theme.d.ts b/explorer-nextjs/app/theme/mui-theme.d.ts deleted file mode 100644 index 6d5f9aabf1..0000000000 --- a/explorer-nextjs/app/theme/mui-theme.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme { } - interface ThemeOptions extends Partial<NymTheme> { } - interface Palette extends NymPaletteWithExtensions { } - interface PaletteOptions extends NymPaletteWithExtensionsOptions { } -} diff --git a/explorer-nextjs/app/typeDefs/explorer-api.ts b/explorer-nextjs/app/typeDefs/explorer-api.ts deleted file mode 100644 index b15490008d..0000000000 --- a/explorer-nextjs/app/typeDefs/explorer-api.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; - nymnodes: { - count: number; - roles: { - mixnode: number; - entry: number; - exit_nr: number; - exit_ipr: number; - }; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -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; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer-nextjs/app/typeDefs/filters.ts b/explorer-nextjs/app/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer-nextjs/app/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer-nextjs/app/typeDefs/network.ts b/explorer-nextjs/app/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer-nextjs/app/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer-nextjs/app/typeDefs/tables.ts b/explorer-nextjs/app/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer-nextjs/app/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer-nextjs/app/typings/FC.d.ts b/explorer-nextjs/app/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer-nextjs/app/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer-nextjs/app/typings/jpeg.d.ts b/explorer-nextjs/app/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer-nextjs/app/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer-nextjs/app/typings/json.d.ts b/explorer-nextjs/app/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer-nextjs/app/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/png.d.ts b/explorer-nextjs/app/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer-nextjs/app/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/react-identicons.d.ts b/explorer-nextjs/app/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer-nextjs/app/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer-nextjs/app/typings/react-tooltip.d.ts b/explorer-nextjs/app/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer-nextjs/app/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer-nextjs/app/typings/svg.d.ts b/explorer-nextjs/app/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer-nextjs/app/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/utils/currency.ts b/explorer-nextjs/app/utils/currency.ts deleted file mode 100644 index 07bdc890de..0000000000 --- a/explorer-nextjs/app/utils/currency.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - 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, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer-nextjs/app/utils/index.ts b/explorer-nextjs/app/utils/index.ts deleted file mode 100644 index e3c3e4ae19..0000000000 --- a/explorer-nextjs/app/utils/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '@/app/typeDefs/explorer-api'; -import { EconomicsRowsType } from '@/app/components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { - const progressBarValue = value?.progressBarValue || 0; - const fieldValue = value.value; - - if (progressBarValue > 100) { - return theme.palette.warning.main; - } - if (field === 'selectionChance') { - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - switch (fieldValue) { - case 'High': - case 'VeryHigh': - return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Good': - case 'Moderate': - return theme.palette.nym.networkExplorer.selectionChance.moderate; - case 'Low': - case 'VeryLow': - return theme.palette.nym.networkExplorer.selectionChance.underModerate; - default: - return theme.palette.nym.wallet.fee; - } - } - return theme.palette.nym.wallet.fee; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - console.log('balance', balance, fee, tx); - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer-nextjs/next.config.mjs b/explorer-nextjs/next.config.mjs deleted file mode 100644 index d642290145..0000000000 --- a/explorer-nextjs/next.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - async redirects() { - return [ - { - source: '/network-components/mixnode/:id', // Match the old URL - destination: '/network-components/nodes/:id', // Redirect to the new URL - permanent: true, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/explorer-nextjs/package.json b/explorer-nextjs/package.json deleted file mode 100644 index 555749547b..0000000000 --- a/explorer-nextjs/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@nymproject/network-explorer", - "version": "1.0.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "build:prod": "yarn --cwd .. build && next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@nymproject/react": "^1.0.0", - "@nymproject/nym-validator-client": "0.18.0", - "next": "14.2.26", - "react": "^18", - "react-dom": "^18", - "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-charts": "^7.22.3" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "eslint": "^8", - "eslint-config-next": "14.1.4", - "typescript": "^5" - } -} diff --git a/explorer-nextjs/public/favicon.ico b/explorer-nextjs/public/favicon.ico deleted file mode 100644 index bc951a1c6f2c406072df036cea6e73163f6a687e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmc&%drVtp6tBjAwtraU57|Idos<T2TaoeT#&c!#aSMIjwzN=Q0WoZ_F&E;7vl}y; zCYa0^L&hsH#z(R!8snn=w?EWG5Hq>~Ef+d5%b>g&1$p%NeV5&G3m5tT=9hdq_kQ=B z^E=<U=R4myJl-?BXL<Yf@et+nDxc%=1Uw#(&tHB23ax*H)`VycJtB_Pbwqd>Xx-n) zt#yjjH3Lnu{O{}%;^v}6adT0w`qr*D_ca-f4$)@v<k{gp%Jcorsq!nHSbhu$UU=ST z#=R5+nPRI)udy|g<nSK09xN-b?YwjN^$Z`G^*Zpo=+$)4Dt^7g)<InCZ$5m;zbvB< z>o5?uW^6<56{qN!y1~PI3zTLhSUpCWL!`J&HO0On^xEC|`)_dLgKOS*tgq5y0@uh0 z;BSon*P>z|U)tMr;?xgzW(`wEUmrAoa>L74Ra^)Q3kz_+=Pvz?&8tfN#-3i>y2xH* z%bzYQhJTT4U3C?F{Nc3?^0K6+#m(f;RFu-PZ&y{)wrnQPWq+@&hnB6aEcfn%OCH<L z<-f7fQK%^|^~&P?UAJE1^xt;>AC7^b<LBU7{9yU*#xJ9TIX`jywRmFw?N;Xp%QGX? z^UKHwOL3mzqnUvw&PRAyapDPakabj;`V3`OpB`$}R8d`;w+6?xZw{_#%c&Jz88uFp zP+d9;)v7C?%p^&Lg$T1jOkGO}ROg1;RcD9Xd8c|ITmL1bo~VbUq+&=oqD4~!$qB`f zmRb)|#TTG8-vhmQz-=%Nv>OZ*PQf7!rS;)nRo!4?-g)PewDc!POtFA4GSVmZnHXSG z;ekj<K2iuu<!vw+29^vY)u>cH+#8G$ME!7d?mJE!QPT}aQwl*45$+e4ZNutwI3yn` z0IjMMj9Q1yq@|(*v4ObYjL*I3bY+)zKrCMn0`9u|<wXGy*|)%`cDM{0NBmYc;C(CK zb-J<&KZBS95!=RQd=j%i0%Ym!ke}ypnFww_;Qy_(I&G5jjvd8j+n~(41^G&ct%4&S z2&=PEUe^t=FK^%H4CZ<o7bO5v_6L{<M<e?`$gTefh|A8Ugj5u3D5vk{Oo=N1lftoN zkoS7eCeh$*%dY+<#5fPu-f&Q5{s<<yy$$<7a-9kz8XmY4(u+9v-QM&mx}SUL`9?IK zlZWwFjyMtScK_++$Fj^fHcg2w1e46}CM5{_s?)4^&kn*U?%Z<w`wn16qVOQ6KCD}I zJQ+qGJz`jCie=fj%(HNRBp7pgkPQyA;_BZmSruP%@?ri!@D*udaE*@8n7FR-csyI; zW4*+&&!Irt-=cW`L6<o1QpotqjN>rwa?yAT^Yc(<Bw=M`*)M*k=mMB?>|F?d{Ai|c zO~d%f-~My6(qLkIjK-RGyP-s{q4yC8zBslB<Buc!<@gj`(EC?OGGKCIg2r~c=fI*> zd36MWpA@6U_{*W-Yffgt)Z`?MJva9l3e-w3HuhPN_(Az!C(VZGsVN%!@$4*^@)SPU zf#S3LA8`Ctn2j(qGfiX9&dh+3kZ!;Z6yF<vL7cyZg?So#W_lX*@{^p{f#NggZ!P~s zaq45Ae+-U&uainRc`<e%`0V`SFaNfo9BN1T7f;-4;n-tvHlzLb@6r5NmWzj-fAJmg zwC^7^>;A#`Yxl2((_XGUgu2Fc|I$97>yN+wm-++a?{@tqL^a@6^%w1fU8+AZwk%`o z`V-^h;ZuL_KrI*-ME$*L0kVPJe~6!M|H0Vc+kgFx`mdrO`Y+42`Y*-?OmY9IJ~z~c z`%jfm|5@^+|J1JbpJ~4Rr#|1&ww?ZyZ3BMB{kNu?GHI#@Tb}gaW2paLM*a7)u554& W_upg@)k0b*(@yr^>+RU-_x}JZi=y2C diff --git a/explorer-nextjs/public/next.svg b/explorer-nextjs/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/explorer-nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/explorer-nextjs/public/vercel.svg b/explorer-nextjs/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/explorer-nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> \ No newline at end of file diff --git a/explorer-nextjs/tsconfig.json b/explorer-nextjs/tsconfig.json deleted file mode 100644 index 701d0e6984..0000000000 --- a/explorer-nextjs/tsconfig.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "CommonJS", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./*" - ], - "@assets/*": [ - "../assets/*" - ] - }, - "moduleResolution": "node", - "target": "ES2017" - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/explorer/.babelrc b/explorer/.babelrc deleted file mode 100644 index 85e1f1ba26..0000000000 --- a/explorer/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/env", "@babel/react"] -} \ No newline at end of file diff --git a/explorer/.editorconfig b/explorer/.editorconfig deleted file mode 100644 index c35daeee9b..0000000000 --- a/explorer/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# See http://editorconfig.org/ -# EditorConfig helps developers define and maintain consistent coding styles -# between different editors and IDEs. The EditorConfig project consists of a -# file format for defining coding styles and a collection of text editor plugins -# that enable editors to read the file format and adhere to defined styles. -# EditorConfig files are easily readable and they work nicely with version -# control systems. - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/explorer/.env.dev b/explorer/.env.dev deleted file mode 100644 index 3cc86ae00c..0000000000 --- a/explorer/.env.dev +++ /dev/null @@ -1,9 +0,0 @@ -# When running the explorer API locally -#EXPLORER_API_URL=http://localhost:8000/v1 - -EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1 -NYM_API_URL=https://sandbox-nym-api1.nymtech.net -VALIDATOR_URL=https://rpc.sandbox.nymtech.net -BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.env.prod b/explorer/.env.prod deleted file mode 100644 index 2e927b2947..0000000000 --- a/explorer/.env.prod +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 -NYM_API_URL=https://validator.nymtech.net -VALIDATOR_URL=https://rpc.nymtech.net -BIG_DIPPER_URL=https://blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx \ No newline at end of file diff --git a/explorer/.env.qa b/explorer/.env.qa deleted file mode 100644 index dd7052ce4d..0000000000 --- a/explorer/.env.qa +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 -NYM_API_URL=https://qa-validator-api.nymtech.net -VALIDATOR_URL=https://qa-validator.nymtech.net -BIG_DIPPER_URL=https://qa-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.eslintrc.js b/explorer/.eslintrc.js deleted file mode 100644 index 5feba99084..0000000000 --- a/explorer/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - extends: [ - '@nymproject/eslint-config-react-typescript' - ], - overrides: [ - { - files: ['*.ts'], - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - } - } - ] -} diff --git a/explorer/.gitignore b/explorer/.gitignore deleted file mode 100644 index b8b1827577..0000000000 --- a/explorer/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -.DS_Store -detailAPI.md -!.env.dev diff --git a/explorer/.nvmrc b/explorer/.nvmrc deleted file mode 100644 index 3c032078a4..0000000000 --- a/explorer/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -18 diff --git a/explorer/.prettierrc b/explorer/.prettierrc deleted file mode 100644 index ccf75b89de..0000000000 --- a/explorer/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "all", - "singleQuote": true, - "printWidth": 120, - "tabWidth": 2 -} diff --git a/explorer/.storybook/main.js b/explorer/.storybook/main.js deleted file mode 100644 index 1c2a975321..0000000000 --- a/explorer/.storybook/main.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-param-reassign */ -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); - -module.exports = { - stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: '@storybook/react', - core: { - builder: 'webpack5', - }, - // webpackFinal: async (config, { configType }) => { - // // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' - // // You can change the configuration based on that. - // // 'PRODUCTION' is used when building the static version of storybook. - webpackFinal: async (config) => { - config.module.rules.forEach((rule) => { - // look for SVG import rule and replace - // NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/ - if (rule.test?.toString().includes('svg')) { - rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/; - } - }); - - // handle asset loading with this - config.module.rules.unshift({ - test: /\.svg(\?.*)?$/i, - issuer: /\.[jt]sx?$/, - use: ['@svgr/webpack'], - }); - - config.resolve.extensions = ['.tsx', '.ts', '.js']; - config.resolve.plugins = [new TsconfigPathsPlugin()]; - - config.resolve.fallback = { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - }; - - config.plugins.push(new ForkTsCheckerWebpackPlugin({ - typescript: { - mode: 'write-references', - diagnosticOptions: { - semantic: true, - syntactic: true, - }, - }, - })); - - // Return the altered config - return config; - }, - features: { - emotionAlias: false, - }, -}; diff --git a/explorer/.storybook/preview.js b/explorer/.storybook/preview.js deleted file mode 100644 index 5d97f3b5ab..0000000000 --- a/explorer/.storybook/preview.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable react/react-in-jsx-scope */ -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { Box } from '@mui/material'; - -export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -} - -const withThemeProvider = (Story, context) => ( - <div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}> - <div> - <NymNetworkExplorerThemeProvider mode="light"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Light mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - <div> - <NymNetworkExplorerThemeProvider mode="dark"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Dark mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - </div> -); - -export const decorators = [withThemeProvider]; diff --git a/explorer/.vscode/settings.json b/explorer/.vscode/settings.json deleted file mode 100644 index 246e9c6e89..0000000000 --- a/explorer/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } -} diff --git a/explorer/CHANGELOG.md b/explorer/CHANGELOG.md deleted file mode 100644 index 8446a3c5b2..0000000000 --- a/explorer/CHANGELOG.md +++ /dev/null @@ -1,47 +0,0 @@ -## UNRELEASED - -- nothing yet - -## [nym-explorer-v1.0.6](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.6) (2023-02-21) - -- Allow user to filter gateways but current and old versions -- Add copy buttons to gateway identity keys - -## [nym-explorer-v1.0.5](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.5) (2023-02-14) - -- NE - link `Owner` field on the node detail page to the account details on NG explorer ([#2923]) -- NE - Upgrade Sandbox and make below changes: ([#2332]) - -[#2923]: https://github.com/nymtech/nym/issues/2923 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.4](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.4) (2023-01-31) - -- Add routing score on gateway list ([#2913]) -- Add gateway's last Routing Score to the gateways list page ([#2186]) -- Upgrade Sandbox and make below changes: ([#2332]) - -[#2913]: https://github.com/nymtech/nym/pull/2913 -[#2186]: https://github.com/nymtech/nym/issues/2186 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.3](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.3) (2023-01-24) - -- Stake Saturation tooltip on node list and node pages updated ([#2877]) -- Sandbox Upgrade : Name change from "Network Explorer" to "Sandbox Explorer", button to mainnet explorer and link to faucet ([#2332)]) - -[#2877]: https://github.com/nymtech/nym/issues/2877 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.2](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.2) (2023-01-17) - -- changing the explorers guru link ([#2820]) - -[#2820]: https://github.com/nymtech/nym/pull/2820 - -## [nym-explorer-v1.0.1](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.1) (2023-01-10) - -- Feat/2161 ne gate version by @gala1234 in https://github.com/nymtech/nym/pull/2743 -- fix(explorer): set gateway bond 6 decimals by @doums in https://github.com/nymtech/nym/pull/2741 -- Feat/2130 tables update rebase by @gala1234 in https://github.com/nymtech/nym/pull/2742 -- fix(explorer,explorer-api): mixnode location by @doums in https://github.com/nymtech/nym-api diff --git a/explorer/README.md b/explorer/README.md deleted file mode 100644 index a7bbc2346a..0000000000 --- a/explorer/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Nym Network Explorer - -The network explorer lets you explore the Nym network. - -## Getting started - -You will need: - -- NodeJS (use `nvm install` to automatically install the correct version) -- `npm` -- `yarn` - -> **Note**: This project ipart of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them. - -From the [root of the repository](../README.md) run the following to build shared packages: - -``` -yarn -yarn build -``` - -From the `explorer` directory of the `nym` monorepo, run: - -``` -cd explorer -yarn start -``` - -You can then open a browser to http://localhost:3000 and start development. - -## Development - -Documentation for developers [can be found here](./docs). - -Several environment variables are required. They can be -provisioned via a `.env` file. For convenience a `.env.dev` is -provided, just copy its content into `.env`. - -#### Required env vars - -``` -EXPLORER_API_URL -NYM_API_URL -VALIDATOR_URL -BIG_DIPPER_URL -CURRENCY_DENOM -CURRENCY_STAKING_DENOM -``` - -## Deployment - -Build the UI with (starting in the repository root): - -``` -yarn -yarn build -cd explorer -yarn build -``` - -The output will be in the `dist` directory. Serve this with `nginx` or `httpd`. - -Make sure you have built the [explorer-api](./explorer-api) and are running it as a service proxied through -`nginx` or `httpd` so that both the UI and API are running on the same host. - -## License - -Please see the [project README](./README.md) and the [LICENSES directory](../LICENSES) for license details for all Nym software. - -## Contributing - -If you would like to contribute to the Network Explorer send us a PR or -[raise an issue on GitHub](https://github.com/nymtech/nym/issues) and tag them with `network-explorer`. - -## Development - -Please see [development docs](./docs) here for more information on the structure and design of this app. diff --git a/explorer/docs/README.md b/explorer/docs/README.md deleted file mode 100644 index bb0d0be99a..0000000000 --- a/explorer/docs/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Nym Network Explorer - Development Docs - -## Getting started - -You will need: - -- NodeJS -- `nvm` - -We use the following: - -- Typescript -- `eslint` -- `webpack` -- `jest` -- `react-material-ui` -- `react` 17 - -## Development mode - -Copy the `.env.prod` file to `.env` to configure your environment. Using the live sandbox Explorer API is the best way to do development, so the prod settings are good. - -Run the following: - -``` -npm install -npm run start -``` - -A development server with hot reloading will be running on http://localhost:3000. - -## Linting - -`eslint` and `prettier` are configured. - -You can lint the code by running: - -``` -npm run lint -``` - -> **Note:** this will only show linting errors and will not fix them - -To fix all linting errors automatically run: - -``` -npm run lint:fix -``` diff --git a/explorer/jest.config.js b/explorer/jest.config.js deleted file mode 100644 index e86e13bab9..0000000000 --- a/explorer/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; diff --git a/explorer/package.json b/explorer/package.json deleted file mode 100644 index d2426f6122..0000000000 --- a/explorer/package.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "@nym/network-explorer", - "version": "1.0.7", - "private": true, - "license": "Apache-2.0", - "scripts": { - "start": "webpack serve --progress --port 3000", - "build": "webpack build --progress --config webpack.prod.js", - "ci:dev": "echo 'Building CI for branch preview...' && yarn --cwd .. build && cp .env.dev .env && yarn build", - "ci:prod": "echo 'Building CI for prod preview...' && yarn --cwd .. build && cp .env.prod .env && yarn build", - "ci": "[ \"$CI_PROD\" = true ] && yarn ci:prod || yarn ci:dev", - "build:serve": "npx serve dist", - "test": "jest", - "test:watch": "jest --watch", - "tsc": "tsc --noEmit true", - "tsc:watch": "tsc --watch --noEmit true", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "prestorybook": "yarn --cwd .. build", - "storybook": "start-storybook -p 6006", - "storybook:build": "build-storybook" - }, - "dependencies": { - "@chain-registry/types": "^0.18.0", - "@cosmjs/cosmwasm-stargate": "^0.32.0", - "@cosmjs/crypto": "^0.32.0", - "@cosmjs/encoding": "^0.32.0", - "@cosmjs/math": "^0.26.2", - "@cosmjs/stargate": "^0.32.0", - "@cosmjs/tendermint-rpc": "^0.32.0", - "@cosmos-kit/core": "^2.8.9", - "@cosmos-kit/keplr-extension": "^2.7.9", - "@cosmos-kit/react": "^2.10.11", - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@interchain-ui/react": "^1.14.2", - "@mui/icons-material": "^5.0.0", - "@mui/material": "^5.0.1", - "@mui/styles": "^5.0.1", - "@mui/system": "^5.0.1", - "@mui/x-data-grid": "^5.0.0-beta.5", - "@nymproject/contract-clients": "1.2.4-rc.1", - "@nymproject/mui-theme": "^1.0.0", - "@nymproject/node-tester": "^1.0.0", - "@nymproject/nym-validator-client": "^0.18.0", - "@nymproject/react": "^1.0.0", - "@nymproject/types": "^1.0.0", - "@tauri-apps/api": "^1.5.1", - "big.js": "^6.2.1", - "bs58": "^5.0.0", - "buffer": "^6.0.3", - "chain-registry": "^1.29.1", - "cosmjs-types": "^0.9.0", - "d3-scale": "^4.0.0", - "date-fns": "^2.24.0", - "i18n-iso-countries": "^6.8.0", - "lodash": "^4.17.21", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^3.1.4", - "react-google-charts": "^3.0.15", - "react-identicons": "^1.2.5", - "react-router-dom": "6", - "react-simple-maps": "^2.3.0", - "react-tooltip": "^4.2.21", - "semver": "^7.3.8", - "use-clipboard-copy": "^0.2.0" - }, - "devDependencies": { - "@babel/core": "^7.15.0", - "@nymproject/eslint-config-react-typescript": "^1.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", - "@storybook/addon-actions": "^6.5.8", - "@storybook/addon-essentials": "^6.5.8", - "@storybook/addon-interactions": "^6.5.8", - "@storybook/addon-links": "^6.5.8", - "@storybook/react": "^6.5.15", - "@storybook/testing-library": "^0.0.9", - "@svgr/webpack": "^6.1.1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@types/d3-fetch": "^3.0.1", - "@types/d3-geo": "^3.0.2", - "@types/d3-scale": "^4.0.1", - "@types/geojson": "^7946.0.8", - "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", - "@types/react-simple-maps": "^1.0.6", - "@types/react-tooltip": "^4.2.4", - "@types/topojson-client": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "babel-loader": "^8.3.0", - "babel-plugin-root-import": "^6.6.0", - "clean-webpack-plugin": "^4.0.0", - "css-loader": "^6.7.3", - "css-minimizer-webpack-plugin": "^3.0.2", - "dotenv-webpack": "^7.0.3", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-storybook": "^0.5.12", - "favicons-webpack-plugin": "^5.0.2", - "file-loader": "^6.2.0", - "fork-ts-checker-webpack-plugin": "^7.2.1", - "html-webpack-plugin": "^5.3.2", - "jest": "^27.1.0", - "mini-css-extract-plugin": "^2.2.2", - "prettier": "^2.8.7", - "react-refresh": "^0.10.0", - "react-refresh-typescript": "^2.0.2", - "style-loader": "^3.3.1", - "ts-jest": "^27.0.5", - "ts-loader": "^9.4.2", - "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "^4.6.2", - "url-loader": "^4.1.1", - "webpack": "^5.75.0", - "webpack-cli": "^4.8.0", - "webpack-dev-server": "^4.5.0", - "webpack-favicons": "^1.3.8", - "webpack-merge": "^5.8.0" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx deleted file mode 100644 index 4e6fcc41e8..0000000000 --- a/explorer/src/App.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { Nav } from './components/Nav'; -import { MobileNav } from './components/MobileNav'; -import { Routes } from './routes/index'; -import { useIsMobile } from './hooks/useIsMobile'; - -export const App: FCWithChildren = () => { - const isMobile = useIsMobile(); - - if (isMobile) { - return ( - <MobileNav> - <Routes /> - </MobileNav> - ); - } - return ( - <Nav> - <Routes /> - </Nav> - ); -}; diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts deleted file mode 100644 index fbb8c18d6b..0000000000 --- a/explorer/src/api/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.EXPLORER_API_URL; -export const NYM_API_BASE_URL = process.env.NYM_API_URL; -export const VALIDATOR_BASE_URL = process.env.VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BIG_DIPPER = process.env.BIG_DIPPER_URL; - -// 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 = `${VALIDATOR_BASE_URL}/validators`; -export const BLOCK_API = `${NYM_API_BASE_URL}/block`; -export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; -export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. -export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this -export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; - -// errors -export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; - -export const NYM_WEBSITE = 'https://nymtech.net'; - -export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru'; - -export const NYM_MIXNET_CONTRACT = - process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; -export const COSMOS_KIT_USE_CHAIN = process.env.COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts deleted file mode 100644 index a2b134d4ae..0000000000 --- a/explorer/src/api/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - GATEWAYS_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - GATEWAYS_EXPLORER_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - const json = await res.json(); - storeInCache('overview-summary', JSON.stringify(json)); - return json; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - 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 fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - const json = await res.json(); - return json; - }; -} - -export const getEnvironment = (): Environment => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer/src/assets/world-110m.json b/explorer/src/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer/src/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer/src/components/ComponentError.tsx b/explorer/src/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer/src/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer/src/components/ContentCard.tsx b/explorer/src/components/ContentCard.tsx deleted file mode 100644 index c78c719ca7..0000000000 --- a/explorer/src/components/ContentCard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material'; -import React, { ReactEventHandler } from 'react'; - -type ContentCardProps = { - title?: React.ReactNode; - subtitle?: string; - Icon?: React.ReactNode; - Action?: React.ReactNode; - errorMsg?: string; - onClick?: ReactEventHandler; -}; - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && <CardHeader title={title || ''} avatar={Icon} action={Action} subheader={subtitle} />} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -); - -ContentCard.defaultProps = { - title: undefined, - subtitle: undefined, - Icon: null, - Action: null, - errorMsg: undefined, - onClick: () => null, -}; diff --git a/explorer/src/components/CustomColumnHeading.tsx b/explorer/src/components/CustomColumnHeading.tsx deleted file mode 100644 index 1f85806fa2..0000000000 --- a/explorer/src/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; - -export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltipInfo?: string }> = ({ - headingTitle, - tooltipInfo, -}) => { - const theme = useTheme(); - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Delegations/ConfirmationModal.tsx b/explorer/src/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer/src/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateIconButton.tsx b/explorer/src/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 36b581a010..0000000000 --- a/explorer/src/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { SxProps } from '@mui/system'; -import { useIsMobile } from '@src/hooks'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium'; - disabled?: boolean; - tooltip?: React.ReactNode; - sx?: SxProps; - onDelegate: () => void; -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile(); - - const handleOnDelegate = () => { - onDelegate(); - }; - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ); - } - - return ( - <Button variant="outlined" size={size} disabled={disabled} onClick={handleOnDelegate} sx={sx}> - Delegate - </Button> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateModal.tsx b/explorer/src/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 3bafd1888f..0000000000 --- a/explorer/src/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react'; -import { Box, SxProps } from '@mui/material'; -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { CurrencyDenom, DecCoin } from '@nymproject/types'; -import { useWalletContext } from '@src/context/wallet'; -import { useDelegationsContext } from '@src/context/delegations'; -import { SimpleModal } from './SimpleModal'; -import { ModalListItem } from './ModalListItem'; -import { DelegationModalProps } from './DelegationModal'; -import { validateAmount } from '../../utils/currency'; -import { urls } from '../../utils'; - -const MIN_AMOUNT_TO_DELEGATE = 10; - -export const DelegateModal: FCWithChildren<{ - mixId: number; - identityKey: string; - header?: string; - buttonText?: string; - rewardInterval?: string; - estimatedReward?: number; - profitMarginPercentage?: string | null; - nodeUptimePercentage?: number | null; - denom: CurrencyDenom; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - onOk?: (delegationModalProps: DelegationModalProps) => void; -}> = ({ mixId, identityKey, onClose, onOk, denom, sx }) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ amount: '10', denom: 'nym' }); - const [isValidated, setValidated] = useState<boolean>(false); - const [errorAmount, setErrorAmount] = useState<string | undefined>(); - - const { address, balance } = useWalletContext(); - const { handleDelegate } = useDelegationsContext(); - - const validate = async () => { - let newValidatedValue = true; - let errorAmountMessage; - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false; - errorAmountMessage = 'Please enter a valid amount'; - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`; - newValidatedValue = false; - } - - if (!amount?.amount.length) { - newValidatedValue = false; - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds'; - newValidatedValue = false; - } - - setErrorAmount(errorAmountMessage); - setValidated(newValidatedValue); - }; - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number; - delegationAmount: string; - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount); - return tx; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }); - try { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }); - - if (!tx) { - throw new Error('Failed to delegate'); - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/tx/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } catch (e) { - console.error('Failed to delegate', e); - onOk({ - status: 'error', - message: (e as Error).message, - }); - } - } - }; - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount); - }; - - React.useEffect(() => { - validate(); - }, [amount, identityKey, mixId]); - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem label="Account balance" value={`${balance.data} NYM`} divider fontWeight={600} /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ); -}; diff --git a/explorer/src/components/Delegations/DelegationModal.tsx b/explorer/src/components/Delegations/DelegationModal.tsx deleted file mode 100644 index db5b51aefe..0000000000 --- a/explorer/src/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Typography, SxProps, Stack } from '@mui/material'; -import { Link } from '@nymproject/react/link/Link'; -import { LoadingModal } from './LoadingModal'; -import { ConfirmationModal } from './ConfirmationModal'; -import { ErrorModal } from './ErrorModal'; - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info'; - message?: string; - transactions?: { - url: string; - hash: string; - }[]; -}; - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean; - onClose: () => void; - sx?: SxProps; - backdropProps?: object; - children?: React.ReactNode; - } -> = ({ status, message, transactions, open, onClose, children, sx, backdropProps }) => { - if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />; - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ); - } - - if (status === 'info') { - return ( - <ConfirmationModal open={open} title="Connect wallet" confirmButton="OK" onConfirm={onClose}> - <Typography>{message}</Typography> - </ConfirmationModal> - ); - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ); -}; diff --git a/explorer/src/components/Delegations/ErrorModal.tsx b/explorer/src/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer/src/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/LoadingModal.tsx b/explorer/src/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer/src/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/ModalDivider.tsx b/explorer/src/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer/src/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer/src/components/Delegations/ModalListItem.tsx b/explorer/src/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer/src/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer/src/components/Delegations/SimpleModal.tsx b/explorer/src/components/Delegations/SimpleModal.tsx deleted file mode 100644 index 88dff4d9d7..0000000000 --- a/explorer/src/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import ErrorOutline from '@mui/icons-material/ErrorOutline'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}); - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void; - label?: string; - fullWidth?: boolean; - sx?: SxProps; -}) => ( - <Button disableFocusRipple size="large" fullWidth={fullWidth} variant="outlined" onClick={onBack} sx={sx}> - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -); - -export const SimpleModal: FCWithChildren<{ - open: boolean; - hideCloseIcon?: boolean; - displayErrorIcon?: boolean; - displayInfoIcon?: boolean; - headerStyles?: SxProps; - subHeaderStyles?: SxProps; - buttonFullWidth?: boolean; - onClose?: () => void; - onOk?: () => Promise<void>; - onBack?: () => void; - header: string | React.ReactNode; - subHeader?: string; - okLabel: string; - backLabel?: string; - backButtonFullWidth?: boolean; - okDisabled?: boolean; - sx?: SxProps; - children?: React.ReactNode; -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile(); - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack direction="row" justifyContent="space-between" alignItems="center"> - {typeof header === 'string' ? ( - <Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}> - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography mt={subHeader ? 0.5 : 0} mb={3} fontSize={12} color={(theme) => theme.palette.text.secondary}> - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}> - {onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />} - {onOk && ( - <Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}> - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ); -}; diff --git a/explorer/src/components/Delegations/index.ts b/explorer/src/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer/src/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer/src/components/Delegations/styles.ts b/explorer/src/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer/src/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx deleted file mode 100644 index 39355d0607..0000000000 --- a/explorer/src/components/DetailTable.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from 'react'; -import { - Link, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableCellProps, -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { Box } from '@mui/system'; -import { unymToNym } from '../utils/currency'; -import { GatewayEnrichedRowType } from './Gateways'; -import { MixnodeRowType } from './MixNodes'; -import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'; - -export type ColumnsType = { - field: string; - title: string; - headerAlign?: TableCellProps['align']; - width?: string | number; - tooltipInfo?: string; -}; - -export interface UniversalTableProps<T = any> { - tableName: string; - columnsData: ColumnsType[]; - rows: T[]; -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ); - } - - if (field === 'bond') { - return unymToNym(val, 6); - } - - if (field === 'owner') { - return ( - <Link underline="none" color="inherit" target="_blank" href={`https://mixnet.explorers.guru/account/${val}`}> - {val} - </Link> - ); - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} />; - } - - return val; -} - -export const DetailTable: FCWithChildren<{ - tableName: string; - columnsData: ColumnsType[]; - rows: MixnodeRowType[] | GatewayEnrichedRowType[]; -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme(); - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, width, tooltipInfo }) => ( - <TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}> - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Box sx={{ display: 'flex', alignItems: 'center' }}> - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - </Box> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx deleted file mode 100644 index 0823ef6579..0000000000 --- a/explorer/src/components/Filters/Filters.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { useMainContext } from '../../context/main'; -import { MixnodeStatusWithAll, toMixnodeStatus } from '../../typeDefs/explorer-api'; -import { EnumFilterKey, TFilterItem, TFilters } from '../../typeDefs/filters'; -import { formatOnSave, generateFilterSchema } from './filterSchema'; -import { Api } from '../../api'; -import { useIsMobile } from '../../hooks/useIsMobile'; -import FiltersButton from './FiltersButton'; - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void; -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])} - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => (val === 100 && id === 'stakeSaturation' ? '>100' : val)} - /> - </Box> -); - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - const isMobile = useIsMobile(); - - const [showFilters, setShowFilters] = useState(false); - const [isFiltered, setIsFiltered] = useState(false); - const [filters, setFilters] = React.useState<TFilters>(); - const [upperSaturationValue, setUpperSaturationValue] = React.useState<number>(100); - - const baseFilters = useRef<TFilters>(); - const prevFilters = useRef<TFilters>(); - - const handleToggleShowFilters = () => setShowFilters(!showFilters); - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes(); - if (allMixnodes) { - setUpperSaturationValue(Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1)); - const initFilters = generateFilterSchema(); - baseFilters.current = initFilters; - prevFilters.current = initFilters; - setFilters(initFilters); - } - }, []); - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue); - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - }; - return undefined; - }); - }; - - const handleOnSave = async () => { - setShowFilters(false); - await filterMixnodes(formatOnSave(filters!), status); - setIsFiltered(true); - prevFilters.current = filters; - }; - - const handleOnCancel = () => { - setShowFilters(false); - setFilters(prevFilters.current); - }; - - const resetFilters = () => { - setFilters(baseFilters.current); - setIsFiltered(false); - prevFilters.current = baseFilters.current; - }; - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(status)); - resetFilters(); - }; - - useEffect(() => { - initialiseFilters(); - }, [initialiseFilters]); - - useEffect(() => { - resetFilters(); - }, [status]); - - if (!filters) return null; - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth> - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ); -}; diff --git a/explorer/src/components/Filters/FiltersButton.tsx b/explorer/src/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer/src/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer/src/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer/src/components/Footer.tsx b/explorer/src/components/Footer.tsx deleted file mode 100644 index 375b5c9b1a..0000000000 --- a/explorer/src/components/Footer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import MuiLink from '@mui/material/Link'; -import { Link } from 'react-router-dom'; -import Typography from '@mui/material/Typography'; -import { Socials } from './Socials'; -import { useIsMobile } from '../hooks/useIsMobile'; -import { NymVpnIcon } from '../icons/NymVpn'; - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile(); - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <MuiLink component={Link} to="http://nymvpn.com" target="_blank" underline="none" marginRight={1}> - <NymVpnIcon /> - </MuiLink> - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts deleted file mode 100644 index 966a3c987e..0000000000 --- a/explorer/src/components/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api'; -import { toPercentInteger } from '../utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; - node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', - node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - 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), - }; -} diff --git a/explorer/src/components/Gateways/VersionDisplaySelector.tsx b/explorer/src/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index e9be02b419..0000000000 --- a/explorer/src/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { FormControl, MenuItem, Select } from '@mui/material'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions; - handleChange: (option: VersionSelectOptions) => void; -}) => { - const isMobile = useIsMobile(); - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem value={VersionSelectOptions.latestVersion} data-testid="show-gateway-latest-version"> - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem value={VersionSelectOptions.olderVersions} data-testid="show-gateway-old-versions"> - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem value={VersionSelectOptions.all} data-testid="show-gateway-all-versions"> - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ); -}; diff --git a/explorer/src/components/Icons.ts b/explorer/src/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer/src/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer/src/components/MixNodes/BondBreakdown.tsx b/explorer/src/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 7fd66efb24..0000000000 --- a/explorer/src/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -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 TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { ExpandMore } from '@mui/icons-material'; -import { currencyToString } from '../../utils/currency'; -import { useMixnodeContext } from '../../context/mixnode'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext(); - const [showDelegations, toggleShowDelegations] = React.useState<boolean>(false); - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }); - const theme = useTheme(); - const isMobile = useIsMobile(); - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }); - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }); - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount); - const delegationsSum = Number(mixNode.data.total_delegation.amount); - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }); - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }); - } - }, [mixNode]); - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations); - } - }; - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount); - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount); - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount; - return ((num * 100) / rawTotalBondsAmount).toFixed(1); - } - return 0; - }; - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert>; - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert>; - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && <ExpandMore />} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left">{currencyToString({ amount: amount.toString() })}</TableCell> - <TableCell align="left">{calcBondPercentage(amount)}%</TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/DetailSection.tsx b/explorer/src/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 6becf5efe4..0000000000 --- a/explorer/src/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; -import Identicon from 'react-identicons'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { MixnodeRowType } from '.'; -import { getMixNodeStatusText, MixNodeStatus } from './Status'; -import { MixNodeDescriptionResponse } from '../../typeDefs/explorer-api'; - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType; - mixnodeDescription: MixNodeDescriptionResponse; -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNodeRow, mixnodeDescription }) => { - const theme = useTheme(); - const palette = [theme.palette.text.primary]; - const isMobile = useIsMobile(); - const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]); - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box display="flex" flexDirection={isMobile ? 'column' : 'row'} width="100%"> - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon size={43} string={mixNodeRow.identity_key} palette={palette} /> - </Box> - <Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}> - <Typography fontSize={21}>{mixnodeDescription.name}</Typography> - <Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography> - <Button - component="a" - variant="text" - sx={{ - mt: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <Grid - item - xs={12} - md={6} - display="flex" - justifyContent={isMobile ? 'start' : 'end'} - mt={isMobile ? 3 : undefined} - > - <Box display="flex" flexDirection="column"> - <Typography fontWeight="600" alignSelf={isMobile ? 'start' : 'self-end'}> - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - width: '15%', - tooltipInfo: - 'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.', - }, - { - field: 'estimatedOperatorReward', - title: 'Estimated Operator Reward', - width: '15%', - tooltipInfo: - "Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.", - }, - { - field: 'selectionChance', - title: 'Active Set Probability', - width: '12.5%', - tooltipInfo: - 'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.', - }, - { - field: 'profitMargin', - title: 'Profit Margin', - width: '12.5%', - tooltipInfo: - 'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.', - }, - { - field: 'operatingCost', - title: 'Operating Cost', - width: '10%', - tooltipInfo: - 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', - }, - { - field: 'nodePerformance', - title: 'Routing Score', - width: '10%', - 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.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index ce947b0a4f..0000000000 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '../../../utils/currency'; -import { useMixnodeContext } from '../../../context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api'; -import { EconomicsInfoRowWithIndex } from './types'; -import { toPercentIntegerString } from '../../../utils'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - const avgUptime = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.last_24h) - : '-'; - const nodePerformance = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.most_recent) - : '-'; - - const opCost = mixNode?.data?.operating_cost; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index 918d024643..0000000000 --- a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useIsMobile } from '../../../hooks/useIsMobile'; -import { EconomicsProgress } from './EconomicsProgress'; - -export const StakeSaturationProgressBar = ({ value, threshold }: { value: number; threshold: number }) => { - const isTablet = useIsMobile('lg'); - const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit'; - const textColor = percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee'; - - return ( - <Box - sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} - id="field" - color={percentageColor} - > - <Typography - sx={{ - mr: isTablet ? 0 : 1, - mb: isTablet ? 1 : 0, - fontWeight: '600', - fontSize: '12px', - color: textColor, - }} - id="stake-saturation-progress-bar" - > - {value}% - </Typography> - <EconomicsProgress value={value} threshold={threshold} color={percentageColor} /> - </Box> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index e6630334db..0000000000 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; -import { Box } from '@mui/system'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'; -import { UniversalTableProps } from '../../DetailTable'; -import { textColour } from '../../../utils'; - -const formatCellValues = (value: EconomicsRowsType, field: string) => ( - <Box sx={{ display: 'flex', alignItems: 'center' }} id="field"> - <Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}> - {value.value} - </Typography> - </Box> -); - -export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({ - tableName, - columnsData, - rows, -}) => { - const theme = useTheme(); - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}> - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index]; - const value: EconomicsRowsType = (eachRow as any)[field]; - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ); - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/index.ts b/explorer/src/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer/src/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer/src/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer/src/components/MixNodes/Status.tsx b/explorer/src/components/MixNodes/Status.tsx deleted file mode 100644 index 627b1f5964..0000000000 --- a/explorer/src/components/MixNodes/Status.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; -import { getMixNodeIcon } from '@src/components/Icons'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; -import { useGetMixNodeStatusColor } from '@src/hooks/useGetMixnodeStatusColor'; - -interface MixNodeStatusProps { - status: MixnodeStatus; -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active'; - case MixnodeStatus.standby: - return 'on standby'; - default: - return 'inactive'; - } -}; - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ status }) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]); - const color = useGetMixNodeStatusColor(status); - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ); -}; diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index 9cb34a2f0c..0000000000 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; -import { MenuItem } from '@mui/material'; -import Select from '@mui/material/Select'; -import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; -import { SxProps } from '@mui/system'; -import { MixNodeStatus } from './Status'; -import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -// TODO: replace with i18n -const ALL_NODES = 'All nodes'; - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll; - sx?: SxProps; - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void; -} - -export const MixNodeStatusDropdown: FCWithChildren<MixNodeStatusDropdownProps> = ({ - status, - onSelectionChanged, - sx, -}) => { - const isMobile = useIsMobile(); - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(status || MixnodeStatusWithAll.all); - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll); - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll); - } - }, - [onSelectionChanged], - ); - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case MixnodeStatusWithAll.active: - case MixnodeStatusWithAll.standby: - case MixnodeStatusWithAll.inactive: - return <MixNodeStatus status={value as unknown as MixnodeStatus} />; - default: - return ALL_NODES; - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem value={MixnodeStatus.active} data-testid="mixnodeStatusSelectOption_active"> - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem value={MixnodeStatus.standby} data-testid="mixnodeStatusSelectOption_standby"> - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem value={MixnodeStatus.inactive} data-testid="mixnodeStatusSelectOption_inactive"> - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={MixnodeStatusWithAll.all} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ); -}; - -MixNodeStatusDropdown.defaultProps = { - onSelectionChanged: undefined, - status: undefined, - sx: undefined, -}; diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer/src/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer/src/components/MixNodes/mappings.ts b/explorer/src/components/MixNodes/mappings.ts deleted file mode 100644 index aba4c6fdca..0000000000 --- a/explorer/src/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '../../utils'; -import { unymToNym } from '../../utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer/src/components/MobileNav.tsx b/explorer/src/components/MobileNav.tsx deleted file mode 100644 index ac4b313467..0000000000 --- a/explorer/src/components/MobileNav.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; -import { AppBar, Box, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, Toolbar } from '@mui/material'; -import { Menu } from '@mui/icons-material'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { ExpandableButton } from './Nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; -import NetworkTitle from './NetworkTitle'; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme(); - const { navState, updateNavState } = useMainContext(); - const [drawerOpen, setDrawerOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const isSmallMobile = useIsMobile(400); - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen); - }; - - const handleClick = (url: string) => { - updateNavState(url); - toggleDrawer(); - }; - - const openDrawer = () => { - setDrawerOpen(true); - }; - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <ConnectKeplrWallet /> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={drawerOpen === true} - drawIsFixed={false} - Icon={props.Icon} - setToActive={handleClick} - nested={props.nested} - isMobile - isActive={props.isActive} - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Nav.tsx b/explorer/src/components/Nav.tsx deleted file mode 100644 index a0f5b16fec..0000000000 --- a/explorer/src/components/Nav.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material'; -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import MuiLink from '@mui/material/Link'; -import Box from '@mui/material/Box'; -import ListItem from '@mui/material/ListItem'; -import MuiDrawer from '@mui/material/Drawer'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; -import List from '@mui/material/List'; -import Typography from '@mui/material/Typography'; -import IconButton from '@mui/material/IconButton'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { NYM_WEBSITE } from '../api/constants'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { DarkLightSwitchDesktop } from './Switch'; -import { NavOptionType } from '../context/nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; - -const drawerWidth = 255; -const bannerHeight = 80; - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}); - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}); - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})); - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})); - -type ExpandableButtonType = { - title: string; - url: string; - isActive?: boolean; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isChild?: boolean; - openDrawer: () => void; - closeDrawer?: () => void; - drawIsTempOpen: boolean; - drawIsFixed: boolean; - fixDrawerClose?: () => void; - isMobile: boolean; - setToActive: (url: string) => void; -}; - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - url, - setToActive, - isActive, - openDrawer, - closeDrawer, - drawIsTempOpen, - drawIsFixed, - fixDrawerClose, - Icon, - title, - nested, - isMobile, - isChild, -}) => { - const [dynamicStyle, setDynamicStyle] = React.useState({}); - const [nestedOptions, toggleNestedOptions] = React.useState(false); - const [isExternal, setIsExternal] = React.useState<boolean>(false); - const { palette } = useTheme(); - - const handleClick = () => { - setToActive(url); - if (title === 'Network Components' && nested) { - openDrawer(); - toggleNestedOptions(!nestedOptions); - } - if (!nested && !drawIsFixed) { - closeDrawer?.(); - } - if (!nested && isMobile) { - fixDrawerClose?.(); - } - }; - - React.useEffect(() => { - if (url) { - setIsExternal(url.includes('http')); - } - if (nested) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - if (isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.nested, - fontWeight: 600, - }); - } - if (!nested && !isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - }, [url]); - - React.useEffect(() => { - if (!drawIsTempOpen && nestedOptions) { - toggleNestedOptions(false); - } - }, [drawIsTempOpen]); - - const linkProps = isExternal - ? { - component: 'a', - href: url, - target: '_blank', - } - : { component: !nested ? Link : 'div', to: url }; - - return ( - <> - <ListItem - disablePadding - disableGutters - {...linkProps} - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(isActive - ? dynamicStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={handleClick} - sx={{ - pt: 2, - pb: 2, - background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - primaryTypographyProps={{ - style: { - fontWeight: isActive ? 600 : 400, - }, - }} - /> - {nested && nestedOptions && <ExpandLess />} - {nested && !nestedOptions && <ExpandMore />} - </ListItemButton> - </ListItem> - {nestedOptions && - nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - setToActive={setToActive} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - /> - ))} - </> - ); -}; - -ExpandableButton.defaultProps = { - Icon: null, - nested: undefined, - isChild: false, - isActive: false, - fixDrawerClose: undefined, - closeDrawer: undefined, -}; - -export const Nav: FCWithChildren = ({ children }) => { - const { updateNavState, navState, environment } = useMainContext(); - const [drawerIsOpen, setDrawerToOpen] = React.useState(false); - const [fixedOpen, setFixedOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const theme = useTheme(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - - const setToActive = (url: string) => { - updateNavState(url); - }; - - const fixDrawerOpen = () => { - setFixedOpen(true); - setDrawerToOpen(true); - }; - - const fixDrawerClose = () => { - setFixedOpen(false); - setDrawerToOpen(false); - }; - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true); - } - }; - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false); - } - }; - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} height={bannerHeight} /> - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - <NymLogo height="40px" width="40px" /> - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/" underline="none" color="inherit"> - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ borderRadius: 2, textTransform: 'none', width: 150, ml: 4, fontSize: 14, fontWeight: 600 }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={drawerIsOpen} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List sx={{ pt: 0, pb: 0 }} onMouseEnter={tempDrawerOpen} onMouseLeave={tempDrawerClose}> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - setToActive={setToActive} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box sx={{ width: '100%', py: 5, px: 6, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/NetworkTitle.tsx b/explorer/src/components/NetworkTitle.tsx deleted file mode 100644 index 7dd87d7e42..0000000000 --- a/explorer/src/components/NetworkTitle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Button, Typography } from '@mui/material'; -import { Link } from 'react-router-dom'; -import MuiLink from '@mui/material/Link'; -import { useMainContext } from '@src/context/main'; - -type NetworkTitleProps = { - showToggleNetwork?: boolean; -}; - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/overview" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ textTransform: 'none', width: 114, fontSize: '12px', fontWeight: 600, ml: 1 }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ); -}; - -export default NetworkTitle; diff --git a/explorer/src/components/Socials.tsx b/explorer/src/components/Socials.tsx deleted file mode 100644 index e1bb33970b..0000000000 --- a/explorer/src/components/Socials.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { Box, IconButton } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { TelegramIcon } from '../icons/socials/TelegramIcon'; -import { GitHubIcon } from '../icons/socials/GitHubIcon'; -import { TwitterIcon } from '../icons/socials/TwitterIcon'; -import { DiscordIcon } from '../icons/socials/DiscordIcon'; - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ isFooter }) => { - const theme = useTheme(); - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons; - return ( - <Box> - <IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram"> - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord"> - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter"> - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={GITHUB_LINK} target="_blank" data-testid="github"> - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ); -}; - -Socials.defaultProps = { - isFooter: false, -}; diff --git a/explorer/src/components/StatsCard.tsx b/explorer/src/components/StatsCard.tsx deleted file mode 100644 index 36355b1759..0000000000 --- a/explorer/src/components/StatsCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import EastIcon from '@mui/icons-material/East'; - -interface StatsCardProps { - icon: React.ReactNode; - title: string; - count?: string | number; - errorMsg?: Error | string; - onClick?: () => void; - color?: string; -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography ml={3} mr={0.75} fontSize="inherit" fontWeight="inherit" data-testid={`${title}-amount`}> - {count === undefined || count === null ? '' : count} - </Typography> - <Typography mr={1} fontSize="inherit" fontWeight="inherit" data-testid={title}> - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' ? errorMsg : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ); -}; - -StatsCard.defaultProps = { - onClick: undefined, - errorMsg: undefined, - color: undefined, - count: undefined, -}; diff --git a/explorer/src/components/StyledLink.tsx b/explorer/src/components/StyledLink.tsx deleted file mode 100644 index fd101e5b4a..0000000000 --- a/explorer/src/components/StyledLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Link as MuiLink, SxProps } from '@mui/material'; -import { Link as RRDL } from 'react-router-dom'; - -type StyledLinkProps = { - to: string; - children: string; - target?: React.HTMLAttributeAnchorTarget; - dataTestId?: string; - color?: string; - sx?: SxProps; -}; - -const StyledLink = ({ to, children, dataTestId, target, color = 'inherit', sx }: StyledLinkProps) => ( - <MuiLink - sx={{ ...sx }} - color={color} - target={target} - underline="none" - component={RRDL} - to={to} - data-testid={dataTestId} - > - {children} - </MuiLink> -); - -export default StyledLink; diff --git a/explorer/src/components/Switch.tsx b/explorer/src/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer/src/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx deleted file mode 100644 index d3af522168..0000000000 --- a/explorer/src/components/TableToolbar.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { Box, TextField, MenuItem, FormControl, IconButton, Select, SelectChangeEvent } from '@mui/material'; -import { Close } from '@mui/icons-material'; -import { Filters } from './Filters/Filters'; -import { useIsMobile } from '../hooks/useIsMobile'; - -const fieldsHeight = '42.25px'; - -type TableToolBarProps = { - onChangeSearch?: (arg: string) => void; - onChangePageSize: (event: SelectChangeEvent<string>) => void; - pageSize: string; - searchTerm?: string; - withFilters?: boolean; - childrenBefore?: React.ReactNode; - childrenAfter?: React.ReactNode; -}; - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - searchTerm, - onChangeSearch, - onChangePageSize, - pageSize, - childrenBefore, - childrenAfter, - withFilters, -}) => { - const isMobile = useIsMobile(); - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}> - <Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}> - {childrenBefore} - <FormControl size="small"> - <Select - value={pageSize} - onChange={onChangePageSize} - sx={{ - width: isMobile ? '100%' : 200, - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem value={10} data-testid="ten"> - 10 - </MenuItem> - <MenuItem value={30} data-testid="thirty"> - 30 - </MenuItem> - <MenuItem value={50} data-testid="fifty"> - 50 - </MenuItem> - <MenuItem value={100} data-testid="hundred"> - 100 - </MenuItem> - </Select> - </FormControl> - </Box> - {!!onChangeSearch && ( - <TextField - sx={{ - width: isMobile ? '100%' : 200, - marginBottom: isMobile ? 2 : 0, - }} - size="small" - value={searchTerm} - data-testid="search-box" - placeholder="Search" - InputProps={{ - endAdornment: searchTerm?.length ? ( - <IconButton size="small" onClick={() => onChangeSearch('')}> - <Close fontSize="small" /> - </IconButton> - ) : undefined, - }} - onChange={(event) => onChangeSearch(event.target.value)} - /> - )} - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {withFilters && <Filters />} - {childrenAfter} - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Title.tsx b/explorer/src/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer/src/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer/src/components/Tooltip.tsx b/explorer/src/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer/src/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer/src/components/TwoColSmallTable.tsx b/explorer/src/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer/src/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -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 CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer/src/components/Universal-DataGrid.tsx b/explorer/src/components/Universal-DataGrid.tsx deleted file mode 100644 index 6598dfb485..0000000000 --- a/explorer/src/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import * as React from 'react'; -import { makeStyles } from '@mui/styles'; -import { DataGrid, GridColDef, GridEventListener, useGridApiContext, useGridState } from '@mui/x-data-grid'; -import Pagination from '@mui/material/Pagination'; -import { LinearProgress } from '@mui/material'; -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}); - -const CustomPagination = () => { - const apiRef = useGridApiContext(); - const [state] = useGridState(apiRef); - - const classes = useStyles(); - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={state.pagination.pageCount} - page={state.pagination.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ); -}; - -type DataGridProps = { - columns: GridColDef[]; - pagination?: true | undefined; - pageSize?: string | undefined; - rows: any; - loading?: boolean; - initialState?: GridInitialStateCommunity; - onRowClick?: GridEventListener<'rowClick'> | undefined; -}; -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress />; - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - components={{ - Pagination: CustomPagination, - }} - columns={columns} - pageSize={Number(pageSize)} - disableSelectionOnClick - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ); -}; - -UniversalDataGrid.defaultProps = { - loading: false, - pagination: undefined, - pageSize: '10', -}; diff --git a/explorer/src/components/UptimeChart.tsx b/explorer/src/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer/src/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx b/explorer/src/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index ff5d2691c6..0000000000 --- a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Button, IconButton, Stack, CircularProgress } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useWalletContext } from '@src/context/wallet'; -import { WalletAddress, WalletBalance } from '@src/components/Wallet'; - -export const ConnectKeplrWallet = () => { - const { connectWallet, disconnectWallet, isWalletConnected, isWalletConnecting } = useWalletContext(); - const isMobile = useIsMobile(1200); - - if (!connectWallet || !disconnectWallet) { - return null; - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet(); - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ); - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={isWalletConnecting && <CircularProgress size={14} color="inherit" />} - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ); -}; diff --git a/explorer/src/components/Wallet/WalletAddress.tsx b/explorer/src/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 8be6c9f291..0000000000 --- a/explorer/src/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { ElipsSVG } from '@src/icons/ElipsSVG'; -import { trimAddress } from '@src/utils'; -import { useWalletContext } from '@src/context/wallet'; - -export const WalletAddress = () => { - const { address } = useWalletContext(); - - const displayAddress = trimAddress(address, 7); - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/WalletBalance.tsx b/explorer/src/components/Wallet/WalletBalance.tsx deleted file mode 100644 index f94935ac23..0000000000 --- a/explorer/src/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useWalletContext } from '@src/context/wallet'; -import { useIsMobile } from '@src/hooks'; -import { TokenSVG } from '@src/icons/TokenSVG'; - -export const WalletBalance = () => { - const { balance } = useWalletContext(); - const isMobile = useIsMobile(1200); - - const showBalance = !isMobile && balance.status === 'success'; - - if (!showBalance) { - return null; - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/index.ts b/explorer/src/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer/src/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer/src/components/WorldMap.tsx b/explorer/src/components/WorldMap.tsx deleted file mode 100644 index 7a86f34095..0000000000 --- a/explorer/src/components/WorldMap.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import * as React from 'react'; -import { scaleLinear } from 'd3-scale'; -import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'; -import ReactTooltip from 'react-tooltip'; -import { CircularProgress } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api'; -import MAP_TOPOJSON from '../assets/world-110m.json'; - -type MapProps = { - userLocation?: [number, number]; - countryData?: ApiState<CountryDataResponse>; - loading: boolean; -}; - -export const WorldMap: FCWithChildren<MapProps> = ({ countryData, userLocation, loading }) => { - const { palette } = useTheme(); - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max(...Object.values(countryData.data).map((country) => country.nodes)); - return scaleLinear<string, string>() - .domain([0, 1, heighestNumberOfNodes / 4, heighestNumberOfNodes / 2, heighestNumberOfNodes]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]); - } - return () => palette.nym.networkExplorer.map.fills[0]; - }, [countryData, palette]); - - const [tooltipContent, setTooltipContent] = React.useState<string | null>(null); - - if (loading) { - return <CircularProgress />; - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3]; - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties; - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`); - } - }} - onMouseLeave={() => { - setTooltipContent(''); - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ); - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ); -}; - -WorldMap.defaultProps = { - userLocation: undefined, - countryData: undefined, -}; diff --git a/explorer/src/components/delegatorsInfo/types.ts b/explorer/src/components/delegatorsInfo/types.ts deleted file mode 100644 index b0e3fd6e99..0000000000 --- a/explorer/src/components/delegatorsInfo/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type RowsType = { - value?: string | number; - visualProgressValue?: number; -}; - -export interface DelegatorsInfoRow { - estimated_total_reward: RowsType; - estimated_operator_reward: RowsType; - active_set_probability: RowsType; - stake_saturation: RowsType; - profit_margin: RowsType; - avg_uptime: RowsType; -} - -export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number }; diff --git a/explorer/src/components/index.ts b/explorer/src/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer/src/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer/src/context/cosmos-kit.tsx b/explorer/src/context/cosmos-kit.tsx deleted file mode 100644 index ce82b21a17..0000000000 --- a/explorer/src/context/cosmos-kit.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { ChainProvider } from '@cosmos-kit/react'; -import { wallets as keplr } from '@cosmos-kit/keplr-extension'; -import { assets, chains } from 'chain-registry'; -import { Chain, AssetList } from '@chain-registry/types'; -import { VALIDATOR_BASE_URL } from '@src/api/constants'; - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -}; - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -}; - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx'); - - return nyx ? [nymSandbox, nyx] : [nymSandbox]; - }, [chains]); - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx'); - - return nyx ? [nyx] : [nymSandboxAssets]; - }, [assets]) as AssetList[]; - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ); -}; - -export default CosmosKitProvider; diff --git a/explorer/src/context/delegations.tsx b/explorer/src/context/delegations.tsx deleted file mode 100644 index 28a50345e0..0000000000 --- a/explorer/src/context/delegations.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { Delegation, PendingEpochEvent, PendingEpochEventKind } from '@nymproject/contract-clients/Mixnet.types'; -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'; -import { useWalletContext } from './wallet'; -import { useMainContext } from './main'; - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] }; - -export type PendingEvent = ReturnType<typeof getEventsByAddress>; - -export type DelegationWithRewards = Delegation & { - rewards: string; - identityKey: string; - pending: PendingEvent; -}; - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - }; - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - }; - } - - return undefined; -}; - -interface DelegationsState { - delegations?: DelegationWithRewards[]; - handleGetDelegations: () => Promise<void>; - handleDelegate: (mixId: number, amount: string) => Promise<ExecuteResult | undefined>; - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined>; -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet'); - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet'); - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const DelegationsProvider = ({ children }: { children: React.ReactNode }) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>(); - const { address, nymQueryClient, nymClient } = useWalletContext(); - const { fetchMixnodes } = useMainContext(); - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingEpochEvents({}); - const pendingEvents: PendingEvent[] = []; - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address); - if (event) { - pendingEvents.push(event); - } - }); - - return pendingEvents; - }; - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingDelegatorReward({ address, mixId }); - - return response; - }; - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined); - return undefined; - } - - if (!address) { - setDelegations(undefined); - return undefined; - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes(); - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ delegator: address }); - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => handleGetDelegationRewards(d.mix_id)), - ); - - // Get all pending events - const pendingEvents = await handleGetPendingEvents(); - - const delegationsWithRewards: DelegationWithRewards[] = []; - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => (e?.mixId === d.mix_id ? e.kind : undefined)), - identityKey: mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }); - }); - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if (e && !delegationsWithRewards.find((d: DelegationWithRewards) => d.mix_id === e.mixId)) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node.identity_key || '', - pending: e, - }); - } - }); - - setDelegations(delegationsWithRewards); - - return undefined; - }, [address, nymQueryClient]); - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const amountToDelegate = (Number(amount) * 1000000).toString(); - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }]; - try { - const tx = await nymClient?.delegateToMixnode({ mixId }, fee, 'Delegation from Nym Explorer', uNymFunds); - - return tx as unknown as ExecuteResult; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode({ mixId }, fee, 'Undelegation from Nym Explorer'); - - return tx as unknown as ExecuteResult; - }; - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations], - ); - - return <DelegationsContext.Provider value={contextValue}>{children}</DelegationsContext.Provider>; -}; - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext); - if (!context) { - throw new Error('useDelegationsContext must be used within a DelegationsProvider'); - } - return context; -}; diff --git a/explorer/src/context/gateway.tsx b/explorer/src/context/gateway.tsx deleted file mode 100644 index dd96634518..0000000000 --- a/explorer/src/context/gateway.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface GatewayState { - uptimeReport?: ApiState<GatewayReportResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const GatewayContext = React.createContext<GatewayState>({}); - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext); - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string; - children: JSX.Element; -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById(); - clearUptimeHistory(); - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]); - }, [gatewayIdentityKey]); - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory], - ); - - return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>; -}; diff --git a/explorer/src/context/hooks.ts b/explorer/src/context/hooks.ts deleted file mode 100644 index 9c5579c899..0000000000 --- a/explorer/src/context/hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { ApiState } from '../typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx deleted file mode 100644 index c7a74a3d0d..0000000000 --- a/explorer/src/context/main.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import * as React from 'react'; -import { PaletteMode } from '@mui/material'; -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '../typeDefs/explorer-api'; -import { EnumFilterKey } from '../typeDefs/filters'; -import { Api, getEnvironment } from '../api'; -import { NavOptionType, originalNavOptions } from './nav'; -import { toPercentIntegerString } from '../utils'; - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse>; - block?: ApiState<BlockResponse>; - countryData?: ApiState<CountryDataResponse>; - gateways?: ApiState<GatewayResponse>; - globalError?: string | undefined; - mixnodes?: ApiState<MixNodeResponse>; - mode: PaletteMode; - navState: NavOptionType[]; - validators?: ApiState<ValidatorsResponse>; - environment?: Environment; - serviceProviders?: ApiState<DirectoryServiceProvider[]>; -} - -interface StateApi { - fetchMixnodes: (status?: MixnodeStatus) => Promise<MixNodeResponse | undefined>; - filterMixnodes: (filters: any, status: any) => void; - toggleMode: () => void; - updateNavState: (title: string) => void; -} - -type State = StateData & StateApi; - -export const MainContext = React.createContext<State>({ - mode: 'dark', - updateNavState: () => null, - navState: originalNavOptions, - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), -}); - -export const useMainContext = (): React.ContextType<typeof MainContext> => React.useContext<State>(MainContext); - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>(); - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark'); - - // nav state - const [navState, updateNav] = React.useState<NavOptionType[]>(originalNavOptions); - - // global / banner error messaging - const [globalError] = React.useState<string>(); - - // various APIs for Overview page - const [summaryOverview, setSummaryOverview] = React.useState<ApiState<SummaryOverviewResponse>>(); - const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>(); - const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>(); - const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>(); - const [block, setBlock] = React.useState<ApiState<BlockResponse>>(); - const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>(); - const [serviceProviders, setServiceProviders] = React.useState<ApiState<DirectoryServiceProvider[]>>(); - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')); - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary(); - setSummaryOverview({ data, isLoading: false }); - } catch (error) { - setSummaryOverview({ - error: error instanceof Error ? error : new Error('Overview summary api fail'), - isLoading: false, - }); - } - }; - - const fetchMixnodes = async (status?: MixnodeStatus) => { - let data; - setMixnodes((d) => ({ ...d, isLoading: true })); - try { - data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - setMixnodes({ data, isLoading: false }); - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }); - } - return data; - }; - - const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => { - setMixnodes((d) => ({ ...d, isLoading: true })); - const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1], - ); - - setMixnodes({ data: filtered, isLoading: false }); - }; - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })); - try { - const data = await Api.fetchGateways(); - setGateways({ data, isLoading: false }); - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }); - } - }; - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators(); - setValidators({ data, isLoading: false }); - } catch (error) { - setValidators({ - error: error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }); - } - }; - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock(); - setBlock({ data, isLoading: false }); - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }); - } - }; - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchCountryData(); - setCountryData({ data: res, isLoading: false }); - } catch (error) { - setCountryData({ - error: error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }); - } - }; - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchServiceProviders(); - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })); - setServiceProviders({ data: resWithRoutingScorePercentage, isLoading: false }); - } catch (error) { - setServiceProviders({ - error: error instanceof Error ? error : new Error('Service provider api fail'), - isLoading: false, - }); - } - }; - - const updateNavState = (url: string) => { - const updated = navState.map((option) => ({ - ...option, - isActive: option.url === url, - })); - updateNav(updated); - }; - - React.useEffect(() => { - if (environment === 'mainnet') { - fetchServiceProviders(); - } - }, [environment]); - - React.useEffect(() => { - setEnvironment(getEnvironment()); - Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]); - }, []); - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - fetchMixnodes, - filterMixnodes, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - toggleMode, - updateNavState, - validators, - serviceProviders, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - validators, - serviceProviders, - ], - ); - - return <MainContext.Provider value={state}>{children}</MainContext.Provider>; -}; diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx deleted file mode 100644 index 36e43d568b..0000000000 --- a/explorer/src/context/mixnode.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import * as React from 'react'; -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; -import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes'; - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse>; - uniqDelegations?: ApiState<UniqDelegationsResponse>; - description?: ApiState<MixNodeDescriptionResponse>; - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>; - mixNode?: ApiState<MixNodeResponseItem | undefined>; - mixNodeRow?: MixnodeRowType; - stats?: ApiState<StatsResponse>; - status?: ApiState<StatusResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const MixnodeContext = React.createContext<MixnodeState>({}); - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext); - -interface MixnodeContextProviderProps { - mixId: string; - children: React.ReactNode; -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren<MixnodeContextProviderProps> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState<MixNodeResponseItem | undefined>( - mixId, - Api.fetchMixnodeByID, - 'Failed to fetch mixnode by id', - ); - - const [mixNodeRow, setMixnodeRow] = React.useState<MixnodeRowType | undefined>(); - - const [delegations, fetchDelegations, clearDelegations] = useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status', - ); - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats', - ); - - const [description, fetchDescription, clearDescription] = useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description', - ); - - const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] = - useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById(); - clearDelegations(); - clearUniqDelegations(); - clearStatus(); - clearStats(); - clearDescription(); - clearEconomicDynamicsStats(); - clearUptimeHistory(); - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined); - return; - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)); - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]); - }); - }, [mixId]); - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ], - ); - - return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>; -}; diff --git a/explorer/src/context/nav.tsx b/explorer/src/context/nav.tsx deleted file mode 100644 index 6df749f585..0000000000 --- a/explorer/src/context/nav.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; -import { BIG_DIPPER } from '../api/constants'; -import { OverviewSVG } from '../icons/OverviewSVG'; -import { NodemapSVG } from '../icons/NodemapSVG'; -import { NetworkComponentsSVG } from '../icons/NetworksSVG'; - -export type NavOptionType = { - isActive?: boolean; - url: string; - title: string; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isExpandedChild?: boolean; -}; - -export const originalNavOptions: NavOptionType[] = [ - { - isActive: false, - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - isActive: false, - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/mixnodes', - title: 'Mixnodes', - }, - { - url: '/network-components/gateways', - title: 'Gateways', - }, - { - url: `${BIG_DIPPER}/validators`, - title: 'Validators', - }, - { - url: 'network-components/service-providers', - title: 'Service Providers', - }, - ], - }, - { - isActive: false, - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - isActive: false, - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon />, - }, -]; diff --git a/explorer/src/context/wallet.tsx b/explorer/src/context/wallet.tsx deleted file mode 100644 index 8ac26ee7ca..0000000000 --- a/explorer/src/context/wallet.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { Wallet } from '@cosmos-kit/core'; -import { unymToNym } from '@src/utils/currency'; -import { useNymClient } from '@src/hooks'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN } from '@src/api/constants'; - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string }; - address?: string; - isWalletConnected: boolean; - isWalletConnecting: boolean; - wallet?: Wallet; - nymClient?: MixnetClient; - nymQueryClient?: MixnetQueryClient; - connectWallet: () => Promise<void>; - disconnectWallet: () => Promise<void>; -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet'); - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ status: 'loading', data: undefined }); - - const { connect, disconnect, wallet, address, isWalletConnected, isWalletConnecting, getCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN); - - const { nymClient, nymQueryClient } = useNymClient(address); - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient(); - const uNYMBalance = await account.getBalance(walletAddress, 'unym'); - const NYMBalance = unymToNym(uNYMBalance.amount); - - return NYMBalance; - }; - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress); - setBalance({ status: 'success', data: walletBalance }); - }; - - useEffect(() => { - if (isWalletConnected && address) { - init(address); - } - }, [address, isWalletConnected]); - - const handleConnectWallet = async () => { - await connect(); - }; - - const handleDisconnectWallet = async () => { - await disconnect(); - setBalance({ status: 'loading', data: undefined }); - }; - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [address, balance, wallet, isWalletConnected, isWalletConnecting, nymClient, nymQueryClient], - ); - - return <WalletContext.Provider value={contextValue}>{children}</WalletContext.Provider>; -}; - -export const useWalletContext = () => useContext(WalletContext); diff --git a/explorer/src/errors/ErrorBoundaryContent.tsx b/explorer/src/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 0389588e26..0000000000 --- a/explorer/src/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { FallbackProps } from 'react-error-boundary'; -import { Alert, AlertTitle, Container } from '@mui/material'; -import { NymThemeProvider } from '@nymproject/mui-theme'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ error }) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -); diff --git a/explorer/src/hooks/index.ts b/explorer/src/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer/src/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer/src/hooks/useGetMixnodeStatusColor.ts b/explorer/src/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index d9d71fe7e1..0000000000 --- a/explorer/src/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer/src/hooks/useIsMobile.ts b/explorer/src/hooks/useIsMobile.ts deleted file mode 100644 index 225964b464..0000000000 --- a/explorer/src/hooks/useIsMobile.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer/src/hooks/useIsMounted.ts b/explorer/src/hooks/useIsMounted.ts deleted file mode 100644 index b18f3a72b7..0000000000 --- a/explorer/src/hooks/useIsMounted.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer/src/hooks/useNymClient.tsx b/explorer/src/hooks/useNymClient.tsx deleted file mode 100644 index 26b2f529db..0000000000 --- a/explorer/src/hooks/useNymClient.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { contracts } from '@nymproject/contract-clients'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@src/api/constants'; - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>(); - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>(); - - const { getCosmWasmClient, getSigningCosmWasmClient } = useChain(COSMOS_KIT_USE_CHAIN); - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient(); - const cosmWasmClient = await getCosmWasmClient(); - - const client = new contracts.Mixnet.MixnetClient(cosmWasmSigningClient as any, address, NYM_MIXNET_CONTRACT); - const queryClient = new contracts.Mixnet.MixnetQueryClient(cosmWasmClient as any, NYM_MIXNET_CONTRACT); - - setNymClient(client); - setNymQueryClient(queryClient); - }; - - init(); - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]); - - return { nymClient, nymQueryClient }; -}; diff --git a/explorer/src/icons/DelevateSVG.tsx b/explorer/src/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer/src/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer/src/icons/ElipsSVG.tsx b/explorer/src/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer/src/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer/src/icons/GatewaysSVG.tsx b/explorer/src/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer/src/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/LightSwitchSVG.tsx b/explorer/src/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer/src/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer/src/icons/MixnodesSVG.tsx b/explorer/src/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer/src/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/MobileDrawerClose.tsx b/explorer/src/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer/src/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer/src/icons/NetworksSVG.tsx b/explorer/src/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer/src/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NodemapSVG.tsx b/explorer/src/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer/src/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NymVpn.tsx b/explorer/src/icons/NymVpn.tsx deleted file mode 100644 index 788b78a4f9..0000000000 --- a/explorer/src/icons/NymVpn.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; - -interface DiscordIconProps { - size?: { width: number; height: number }; -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg width={size?.width} height={size?.height} viewBox="0 0 170 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -); - -NymVpnIcon.defaultProps = { - size: { width: 80, height: 12 }, -}; diff --git a/explorer/src/icons/OverviewSVG.tsx b/explorer/src/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer/src/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/TokenSVG.tsx b/explorer/src/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer/src/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/ValidatorsSVG.tsx b/explorer/src/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer/src/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/socials/DiscordIcon.tsx b/explorer/src/icons/socials/DiscordIcon.tsx deleted file mode 100644 index 11a9fd1c67..0000000000 --- a/explorer/src/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface DiscordIconProps { - size?: number | string; - color?: string; -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -DiscordIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/GitHubIcon.tsx b/explorer/src/icons/socials/GitHubIcon.tsx deleted file mode 100644 index a8fde9b0a9..0000000000 --- a/explorer/src/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface GitHubIconProps { - size?: number | string; - color?: string; -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -GitHubIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TelegramIcon.tsx b/explorer/src/icons/socials/TelegramIcon.tsx deleted file mode 100644 index cf150a4a2f..0000000000 --- a/explorer/src/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TelegramIconProps { - size?: number | string; - color?: string; -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ); -}; - -TelegramIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TwitterIcon.tsx b/explorer/src/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 6f0b545f56..0000000000 --- a/explorer/src/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TwitterIconProps { - size?: number | string; - color?: string; -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -TwitterIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/index.html b/explorer/src/index.html deleted file mode 100644 index dd5a4761ee..0000000000 --- a/explorer/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="utf-8" /> - <title>Nym Network Explorer - - - - -
- - - diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx deleted file mode 100644 index 0b76be7ab1..0000000000 --- a/explorer/src/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { ErrorBoundary } from 'react-error-boundary'; -import { MainContextProvider } from './context/main'; -import { NetworkExplorerThemeProvider } from './theme'; -import { ErrorBoundaryContent } from './errors/ErrorBoundaryContent'; -import CosmosKitProvider from './context/cosmos-kit'; -import '@interchain-ui/react/styles'; -import { App } from './App'; -import { WalletProvider } from './context/wallet'; -import './styles.css'; - -const elem = document.getElementById('app'); - -if (elem) { - const root = createRoot(elem); - root.render( - - - - - - - - - - - - - , - ); -} diff --git a/explorer/src/pages/404/index.tsx b/explorer/src/pages/404/index.tsx deleted file mode 100644 index 02f4247fc5..0000000000 --- a/explorer/src/pages/404/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Paper, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { useMainContext } from '../../context/main'; - -export const Page404 = () => { - const navigate = useNavigate(); - const { mode } = useMainContext(); - const theme = useTheme(); - return ( - - - - - - Oh No! - It looks like you might be lost. - - Please try the link again or navigate back to{' '} - - - - - - - ); -}; diff --git a/explorer/src/pages/Delegations/index.tsx b/explorer/src/pages/Delegations/index.tsx deleted file mode 100644 index 32e178f2fc..0000000000 --- a/explorer/src/pages/Delegations/index.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useEffect } from 'react'; -import { Alert, AlertTitle, Box, Button, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'; -import { Link, useNavigate } from 'react-router-dom'; -import { DelegationModal, DelegationModalProps, Title, UniversalDataGrid } from '@src/components'; -import { useWalletContext } from '@src/context/wallet'; -import { GridColDef } from '@mui/x-data-grid'; -import { unymToNym } from '@src/utils/currency'; -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@src/context/delegations'; -import { urls } from '@src/utils'; -import { useClipboard } from 'use-clipboard-copy'; -import { Close } from '@mui/icons-material'; - -const mapToDelegationsRow = (delegation: DelegationWithRewards, index: number) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}); - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard(); - - return ( - - - - } - > - Mobile Delegations Beta - - - This is a beta release for mobile delegations If you have any feedback or feature suggestions contact us at - support@nymte.ch - - - - - ); -}; - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState(); - const [isLoading, setIsLoading] = React.useState(false); - const [showBanner, setShowBanner] = React.useState(true); - - const { isWalletConnected } = useWalletContext(); - const { handleGetDelegations, handleUndelegate, delegations } = useDelegationsContext(); - const navigate = useNavigate(); - - useEffect(() => { - let timeoutId: NodeJS.Timeout; - - const fetchDelegations = async () => { - setIsLoading(true); - try { - await handleGetDelegations(); - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }); - } finally { - setIsLoading(false); - - timeoutId = setTimeout(() => { - fetchDelegations(); - }, 60_000); - } - }; - - fetchDelegations(); - - return () => { - clearTimeout(timeoutId); - }; - }, [handleGetDelegations]); - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending'; - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym(pending.amount.amount)} NYM`; - } - - return undefined; - }; - - const onUndelegate = async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }); - - try { - const tx = await handleUndelegate(mixId); - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/transaction/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }); - } - } - }; - - const columns: GridColDef[] = [ - { - field: 'identity', - headerName: 'Identity Key', - width: 400, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'mix_id', - headerName: 'Mix ID', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'amount', - headerName: 'Amount', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'rewards', - headerName: 'Rewards', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'undelegate', - headerName: '', - minWidth: 150, - flex: 1, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'right', - renderCell: (params) => { - const { pending } = params.row; - - return ( - - {pending ? ( - e.stopPropagation()} - PopperProps={{}} - > - - - ) : ( - - )} - - ); - }, - }, - ]; - - const handleRowClick = (params: any) => { - navigate(`/network-components/mixnode/${params.row.mix_id}`); - }; - - return ( - - {confirmationModalProps && ( - { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations(); - } - setConfirmationModalProps(undefined); - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && setShowBanner(false)} />} - - - <Button variant="contained" color="primary" component={Link} to="/network-components/mixnodes"> - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <UniversalDataGrid - onRowClick={handleRowClick} - rows={delegations?.map(mapToDelegationsRow) || []} - columns={columns} - loading={isLoading} - /> - </Card> - </Box> - ); -}; - -export const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -); diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx deleted file mode 100644 index 3382947512..0000000000 --- a/explorer/src/pages/GatewayDetail/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { GatewayBond } from '../../typeDefs/explorer-api'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { GatewayContextProvider, useGatewayContext } from '../../context/gateway'; -import { useMainContext } from '../../context/main'; -import { Title } from '../../components/Title'; - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - 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', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - 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', - }, -]; - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayBond | undefined }) => { - const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>(); - const [status, setStatus] = React.useState<number[] | undefined>(); - const { uptimeReport, uptimeStory } = useGatewayContext(); - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)); - } - }, [uptimeReport, selectedGateway]); - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]); - } - }, [enrichGateway]); - - return ( - <Box component="main"> - <Title text="Gateway Detail" /> - - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageGatewayDetailGuard: FCWithChildren = () => { - const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>(); - const { gateways } = useMainContext(); - const { id } = useParams<{ id: string | undefined }>(); - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway(gateways.data.find((g) => g.gateway.identity_key === id)); - } - }, [gateways, id]); - - if (gateways?.isLoading) { - return <CircularProgress />; - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageGatewayDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ); -}; diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx deleted file mode 100644 index c04b7eaf40..0000000000 --- a/explorer/src/pages/Gateways/index.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import * as React from 'react'; -import { Box, Card, Grid, Stack } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { SelectChangeEvent } from '@mui/material/Select'; -import { diff, gte, rcompare } from 'semver'; -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip'; -import { useMainContext } from '../../context/main'; -import { gatewayToGridRow } from '../../components/Gateways'; -import { GatewayResponse } from '../../typeDefs/explorer-api'; -import { TableToolbar } from '../../components/TableToolbar'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { unymToNym } from '../../utils/currency'; -import { Tooltip } from '../../components/Tooltip'; -import { NYM_BIG_DIPPER } from '../../api/constants'; -import { splice } from '../../utils'; -import { VersionDisplaySelector, VersionSelectOptions } from '../../components/Gateways/VersionDisplaySelector'; -import StyledLink from '../../components/StyledLink'; - -export const PageGateways: FCWithChildren = () => { - const { gateways } = useMainContext(); - const [filteredGateways, setFilteredGateways] = React.useState<GatewayResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('50'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.all); - - const theme = useTheme(); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.reduce((a: string[], b) => [...a, b.gateway.version], []); - const [lastestVersion] = versions.sort(rcompare); - return lastestVersion; - } - // fallback value - return '2.0.0'; - }, [gateways]); - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'patch' || versionDiff === null; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'major' || versionDiff === 'minor'; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions; - case VersionSelectOptions.olderVersions: - return filterByOlderVersions; - case VersionSelectOptions.all: - return gateways?.data || []; - default: - return []; - } - }, [versionFilter, gateways]); - - React.useEffect(() => { - if (searchTerm === '') { - setFilteredGateways(filteredByVersion); - } else { - const filtered = filteredByVersion.filter((g) => { - if ( - g.gateway.location.toLowerCase().includes(searchTerm) || - g.gateway.identity_key.toLocaleLowerCase().includes(searchTerm) || - g.owner.toLowerCase().includes(searchTerm) - ) { - return g; - } - return null; - }); - - if (filtered) { - setFilteredGateways(filtered); - } - } - }, [searchTerm, gateways?.data, versionFilter]); - - const columns: GridColDef[] = [ - { - field: 'identity_key', - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - headerClassName: 'MuiDataGrid-header-override', - width: 400, - disableColumnMenu: true, - headerAlign: 'center', - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" gap={1}> - <CopyToClipboard smallIcons value={params.value} tooltip={`Copy identity key ${params.value} to clipboard`} /> - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`}>{params.value}</StyledLink> - </Stack> - ), - }, - { - 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/gateway/${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/gateway/${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 - onClick={() => handleSearch(params.value as string)} - 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/gateway/${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/gateway/${params.row.identity_key}`} data-testid="pledge-amount"> - {`${unymToNym(params.value, 6)}`} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - if (gateways?.data) { - return ( - <> - <Box mb={2}> - <Title text="Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <UniversalDataGrid - pagination - rows={gatewayToGridRow(filteredGateways)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - </> - ); - } - return null; -}; diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx deleted file mode 100644 index 5ceaa36867..0000000000 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown'; -import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { WorldMap } from '../../components/WorldMap'; -import { MixNodeDetailSection } from '../../components/MixNodes/DetailSection'; -import { MixnodeContextProvider, useMixnodeContext } from '../../context/mixnode'; -import { Title } from '../../components/Title'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - width: '12.5%', - }, - { - field: 'stake_saturation', - title: 'Stake Saturation', - width: '12.5%', - tooltipInfo: - 'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.', - }, - { - field: 'self_percentage', - width: '10%', - title: 'Bond %', - tooltipInfo: "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -]; - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState: FCWithChildren = () => { - const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext(); - const isMobile = useIsMobile(); - return ( - <Box component="main"> - <Title text="Mixnode Detail" /> - <Grid container spacing={2} mt={1} mb={6}> - <Grid item xs={12}> - {mixNodeRow && description?.data && ( - <MixNodeDetailSection mixNodeRow={mixNodeRow} mixnodeDescription={description.data} /> - )} - {mixNodeRow?.blacklisted && ( - <Typography textAlign={isMobile ? 'left' : 'right'} fontSize="smaller" sx={{ color: 'error.main' }}> - This node is having a poor performance - </Typography> - )} - </Grid> - </Grid> - <Grid container> - <Grid item xs={12}> - <DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <DelegatorsInfoTable - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}> - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && <ComponentError text="There was a problem retrieving this nodes stats." />} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && <ComponentError text="There was a problem retrieving port information" />} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={(status?.data?.ports && Object.values(status.data.ports)) || [false, false, false]} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && <ComponentError text="There was a problem retrieving this mixnode location" />} - {mixNode?.data?.location?.latitude && mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[mixNode.data.location.longitude, mixNode.data.location.latitude]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard: FCWithChildren = () => { - const { mixNode } = useMixnodeContext(); - const { id } = useParams<{ id: string | undefined }>(); - - if (mixNode?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageMixnodeDetailWithState />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageMixnodeDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ); -}; diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx deleted file mode 100644 index 79a88f7053..0000000000 --- a/explorer/src/pages/Mixnodes/index.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import * as React from 'react'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { Stack, Card, Grid, Box, Button } from '@mui/material'; -import { useParams, useNavigate, Link } from 'react-router-dom'; -import { SelectChangeEvent } from '@mui/material/Select'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { useMainContext } from '@src/context/main'; -import { - DelegateIconButton, - DelegationModal, - DelegationModalProps, - DelegateModal, - CustomColumnHeading, - StyledLink, - Title, - UniversalDataGrid, - TableToolbar, - Tooltip, - MixNodeStatusDropdown, - mixnodeToGridRow, -} from '@src/components'; -import { MixNodeResponse, MixnodeStatusWithAll, toMixnodeStatus } from '@src/typeDefs/explorer-api'; -import { NYM_BIG_DIPPER } from '@src/api/constants'; -import { currencyToString } from '@src/utils/currency'; -import { splice } from '@src/utils'; -import { useGetMixNodeStatusColor, useIsMobile } from '@src/hooks'; -import { useWalletContext } from '@src/context/wallet'; -import { DelegationsProvider } from '@src/context/delegations'; - -export const PageMixnodes: FCWithChildren = () => { - const { mixnodes, fetchMixnodes } = useMainContext(); - const [filteredMixnodes, setFilteredMixnodes] = React.useState<MixNodeResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [itemSelectedForDelegation, setItemSelectedForDelegation] = React.useState<{ - mixId: number; - identityKey: string; - }>(); - const [confirmationModalProps, setConfirmationModalProps] = React.useState<DelegationModalProps | undefined>(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - - const navigate = useNavigate(); - const { isWalletConnected } = useWalletContext(); - const isMobile = useIsMobile(); - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined); - setConfirmationModalProps(delegationModalProps); - }; - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - React.useEffect(() => { - if (searchTerm === '' && mixnodes?.data) { - setFilteredMixnodes(mixnodes?.data); - } else { - const filtered = mixnodes?.data?.filter((m) => { - if ( - m.location?.country_name.toLowerCase().includes(searchTerm) || - m.mix_node.identity_key.toLocaleLowerCase().includes(searchTerm) || - m.owner.toLowerCase().includes(searchTerm) - ) { - return m; - } - return null; - }); - if (filtered) { - setFilteredMixnodes(filtered); - } - } - }, [searchTerm, mixnodes?.data, mixnodes?.isLoading]); - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)); - }, [status]); - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - navigate( - newStatus && newStatus !== MixnodeStatusWithAll.all - ? `/network-components/mixnodes/${newStatus}` - : '/network-components/mixnodes', - ); - }; - - const handleOnDelegate = ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }); - } else { - setItemSelectedForDelegation({ identityKey, mixId }); - } - }; - - const columns: GridColDef[] = [ - { - field: 'delegate', - disableColumnMenu: true, - disableReorder: true, - sortable: false, - width: isMobile ? 25 : 100, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => null, - renderCell: (params: GridRenderCellParams) => ( - <DelegateIconButton - size="small" - onDelegate={() => handleOnDelegate({ identityKey: params.row.identity_key, mixId: params.row.mix_id })} - /> - ), - }, - { - field: 'identity_key', - width: 325, - headerName: 'Identity Key', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={params.value} - tooltip={`Copy identity key ${params.value} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - dataTestId="identity-link" - > - {splice(7, 29, params.value)} - </StyledLink> - </Stack> - ), - }, - { - field: 'mix_id', - width: 85, - align: 'center', - hide: true, - headerName: 'Mix ID', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - data-testid="mix-id" - > - {params.value} - </StyledLink> - ), - }, - - { - field: 'bond', - width: 150, - align: 'left', - type: 'number', - disableColumnMenu: true, - headerName: 'Stake', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => <CustomColumnHeading headingTitle="Stake" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'stake_saturation', - width: 185, - align: 'center', - headerName: 'Stake Saturation', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Stake Saturation" - tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} %`}</StyledLink> - ), - }, - { - field: 'pledge_amount', - width: 150, - align: 'left', - type: 'number', - headerName: 'Bond', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'profit_percentage', - width: 145, - align: 'center', - headerName: 'Profit Margin', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'operating_cost', - width: 170, - align: 'center', - headerName: 'Operating Cost', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} NYM`}</StyledLink> - ), - }, - { - field: 'node_performance', - width: 165, - align: 'center', - headerName: 'Routing Score', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <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." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'owner', - width: 120, - headerName: 'Owner', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`${NYM_BIG_DIPPER}/account/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'location', - width: 150, - headerName: 'Location', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - renderCell: (params: GridRenderCellParams) => ( - <Tooltip text={params.value} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(params.row.status), - }} - onClick={() => handleSearch(params.value)} - > - {params.value} - </Box> - </Tooltip> - ), - }, - { - field: 'host', - width: 130, - headerName: 'Host', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Host" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - color={useGetMixNodeStatusColor(params.row.status)} - to={`/network-components/mixnode/${params.row.mix_id}`} - > - {params.value} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown sx={{ mr: 2 }} status={status} onSelectionChanged={handleMixnodeStatusChanged} /> - } - childrenAfter={ - isWalletConnected && ( - <Button fullWidth size="large" variant="outlined" color="primary" component={Link} to="/delegations"> - Delegations - </Button> - ) - } - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - withFilters - /> - <UniversalDataGrid - pagination - loading={Boolean(mixnodes?.isLoading)} - rows={mixnodeToGridRow(filteredMixnodes)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined); - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => handleNewDelegation(delegationModalProps)} - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined); - if (confirmationModalProps.status === 'success') { - navigate('/delegations'); - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ); -}; diff --git a/explorer/src/pages/MixnodesMap/index.tsx b/explorer/src/pages/MixnodesMap/index.tsx deleted file mode 100644 index a7c0048a11..0000000000 --- a/explorer/src/pages/MixnodesMap/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Grid, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { ContentCard } from '../../components/ContentCard'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { WorldMap } from '../../components/WorldMap'; -import { useMainContext } from '../../context/main'; -import { CountryDataRowType, countryDataToGridRow } from '../../utils'; - -export const PageMixnodesMap: FCWithChildren = () => { - const { countryData } = useMainContext(); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [formattedCountries, setFormattedCountries] = React.useState<CountryDataRowType[]>([]); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - const columns: GridColDef[] = [ - { - field: 'countryName', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="country-name">{params.value}</Typography>, - }, - { - field: 'nodes', - renderHeader: () => <CustomColumnHeading headingTitle="Number of Nodes" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Typography data-testid="number-of-nodes">{params.value}</Typography> - ), - }, - { - field: 'percentage', - renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="percentage">{params.value}</Typography>, - }, - ]; - - React.useEffect(() => { - if (countryData?.data && searchTerm === '') { - setFormattedCountries(countryDataToGridRow(Object.values(countryData.data))); - } else if (countryData?.data !== undefined && searchTerm !== '') { - const formatted = countryDataToGridRow(Object.values(countryData?.data)); - const filtered = formatted.filter( - (m) => m?.countryName?.toLowerCase().includes(searchTerm) || m?.ISO3?.toLowerCase().includes(searchTerm), - ); - if (filtered) { - setFormattedCountries(filtered); - } - } - }, [searchTerm, countryData?.data]); - - if (countryData?.isLoading) { - return <CircularProgress />; - } - - if (countryData?.data && !countryData.isLoading) { - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - /> - <UniversalDataGrid - pagination - loading={countryData?.isLoading} - columns={columns} - rows={formattedCountries} - pageSize={pageSize} - /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); - } - - if (countryData?.error) { - return <Alert severity="error">{countryData.error.message}</Alert>; - } - - return null; -}; diff --git a/explorer/src/pages/Overview/index.tsx b/explorer/src/pages/Overview/index.tsx deleted file mode 100644 index 9bc2ebade7..0000000000 --- a/explorer/src/pages/Overview/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -import { Box, Grid, Link, Typography } from '@mui/material'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { useTheme } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; -import { PeopleAlt } from '@mui/icons-material'; -import { WorldMap } from '../../components/WorldMap'; -import { useMainContext } from '../../context/main'; -import { formatNumber } from '../../utils'; -import { BIG_DIPPER } from '../../api/constants'; -import { ValidatorsSVG } from '../../icons/ValidatorsSVG'; -import { GatewaysSVG } from '../../icons/GatewaysSVG'; -import { MixnodesSVG } from '../../icons/MixnodesSVG'; -import { Title } from '../../components/Title'; -import { ContentCard } from '../../components/ContentCard'; -import { StatsCard } from '../../components/StatsCard'; -import { Icons } from '../../components/Icons'; - -export const PageOverview: FCWithChildren = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext(); - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes')} - title="Mixnodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.mixnodes.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes/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={() => navigate('/network-components/mixnodes/standby')} - title="Standby nodes" - color={theme.palette.nym.networkExplorer.mixnodes.status.standby} - icon={<Icons.Mixnodes.Status.Standby />} - count={summaryOverview.data?.mixnodes.activeset.standby} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {gateways && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/gateways')} - title="Gateways" - count={gateways?.data?.length || ''} - errorMsg={gateways?.error} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {serviceProviders && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/service-providers')} - title="Service providers" - icon={<PeopleAlt />} - count={serviceProviders.data?.length} - errorMsg={summaryOverview?.error} - /> - </Grid> - )} - {validators && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => window.open(`${BIG_DIPPER}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BIG_DIPPER}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon fontWeight="inherit" fontSize="inherit" sx={{ ml: 0.5 }} /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); -}; diff --git a/explorer/src/pages/ServiceProviders/index.tsx b/explorer/src/pages/ServiceProviders/index.tsx deleted file mode 100644 index 618aa72924..0000000000 --- a/explorer/src/pages/ServiceProviders/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Box, Button, Card, FormControl, Grid, ListItem, Menu, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { useMainContext } from '../../context/main'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; - -const columns: GridColDef[] = [ - { - headerName: 'Client ID', - field: 'address', - disableColumnMenu: true, - flex: 3, - }, - { - headerName: 'Type', - field: 'service_type', - disableColumnMenu: true, - flex: 1, - }, - { - headerName: 'Routing score', - field: 'routing_score', - disableColumnMenu: true, - flex: 2, - sortingOrder: ['asc', 'desc'], - sortComparator: (a?: string, b?: string) => { - if (!a) return -1; // Place undefined values at the end - if (!b) return 1; // Place undefined values at the end - - const aToNum = parseInt(a, 10); - const bToNum = parseInt(b, 10); - - if (aToNum > bToNum) return 1; - - return -1; // Sort numbers in ascending order - }, - renderCell: (params: GridRenderCellParams) => (!params.value ? '-' : params.value), - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Routing score" - tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours" - /> - ), - }, -]; - -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> - ); -}; - -export const ServiceProviders = () => { - const { serviceProviders } = useMainContext(); - const [pageSize, setPageSize] = React.useState('10'); - - const handleOnPageSizeChange = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <> - <Box mb={2}> - <Title text="Service Providers" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - }} - > - {serviceProviders?.data ? ( - <> - <TableToolbar - onChangePageSize={handleOnPageSizeChange} - pageSize={pageSize} - childrenBefore={<SupportedApps />} - /> - <UniversalDataGrid - pagination - rows={serviceProviders.data} - columns={columns} - pageSize={pageSize} - initialState={{ - sorting: { - sortModel: [ - { - field: 'routing_score', - sort: 'desc', - }, - ], - }, - }} - /> - </> - ) : ( - <Typography>No service providers to display</Typography> - )} - </Card> - </Grid> - </Grid> - </> - ); -}; diff --git a/explorer/src/routes/index.tsx b/explorer/src/routes/index.tsx deleted file mode 100644 index 67ef8b4621..0000000000 --- a/explorer/src/routes/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route } from 'react-router-dom'; -import { Delegations } from '@src/pages/Delegations'; -import { PageOverview } from '../pages/Overview'; -import { PageMixnodesMap } from '../pages/MixnodesMap'; -import { Page404 } from '../pages/404'; -import { NetworkComponentsRoutes } from './network-components'; - -export const Routes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="/" element={<PageOverview />} /> - <Route path="/network-components/*" element={<NetworkComponentsRoutes />} /> - <Route path="/nodemap" element={<PageMixnodesMap />} /> - <Route path="/delegations" element={<Delegations />} /> - <Route path="*" element={<Page404 />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/routes/network-components.tsx b/explorer/src/routes/network-components.tsx deleted file mode 100644 index 266a75aa2f..0000000000 --- a/explorer/src/routes/network-components.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom'; -import { BIG_DIPPER } from '../api/constants'; -import { PageGateways } from '../pages/Gateways'; -import { PageGatewayDetail } from '../pages/GatewayDetail'; -import { PageMixnodeDetail } from '../pages/MixnodeDetail'; -import { PageMixnodes } from '../pages/Mixnodes'; -import { ServiceProviders } from '../pages/ServiceProviders'; - -const ValidatorRoute: FCWithChildren = () => { - const navigate = useNavigate(); - window.open(`${BIG_DIPPER}/validators`); - navigate(-1); - return null; -}; - -export const NetworkComponentsRoutes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="mixnodes/:status" element={<PageMixnodes />} /> - <Route path="mixnodes" element={<PageMixnodes />} /> - <Route path="mixnode/:id" element={<PageMixnodeDetail />} /> - <Route path="gateways" element={<PageGateways />} /> - <Route path="gateway/:id" element={<PageGatewayDetail />} /> - <Route path="validators" element={<ValidatorRoute />} /> - <Route path="service-providers" element={<ServiceProviders />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/stories/Introduction.stories.mdx b/explorer/src/stories/Introduction.stories.mdx deleted file mode 100644 index dafab13de0..0000000000 --- a/explorer/src/stories/Introduction.stories.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - -<Meta title="Introduction" /> - -# Nym Network Explorer Storybook - -This is the Storybook for the Nym Network Explorer. diff --git a/explorer/src/styles.css b/explorer/src/styles.css deleted file mode 100644 index 818211ccea..0000000000 --- a/explorer/src/styles.css +++ /dev/null @@ -1,30 +0,0 @@ -/* last resort for styles that cannot be handled by Material UI and Emotion JS */ - -/* TODO - this override should take place in either -the theme declaration in index.tsx or the style prop -in <DataGrid /> */ - -.MuiDataGrid-columnSeparator { - visibility: hidden; -} - -/* TODO - this should be managed somehow in MUI DataGrid -but documentation doesnt offer a way to do it. Possibly only -included in Data Grid Pro package */ -.MuiDataGrid-header-override { - height: 55px; - min-height: 55px; -} - -/* Again, no offered way to add sx to this specific div to kill the padding -which puts it out of sync with other (sx styled) cells */ -div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer { - padding-left: 0; -} - -@media screen and (max-width: 900px) { - .MuiDrawer-paperAnchorLeft { - min-width: 100vw; - margin-top: 58px; - } -} diff --git a/explorer/src/tests/Nav.test.tsx b/explorer/src/tests/Nav.test.tsx deleted file mode 100644 index c3e5e949d0..0000000000 --- a/explorer/src/tests/Nav.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { render } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { Nav } from '../components/Nav'; - -describe('Nav', () => { - beforeEach(() => { - render(<Nav />); - }); - it('should render without exploding', () => { - const { container } = render(<Nav />); - expect(container.firstChild).toBeInTheDocument(); - }); -}); diff --git a/explorer/src/tests/WorldMap.test.tsx b/explorer/src/tests/WorldMap.test.tsx deleted file mode 100644 index 1a5b37d25d..0000000000 --- a/explorer/src/tests/WorldMap.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { WorldMap } from '../components/WorldMap'; - -describe('WorldMap', () => { - beforeEach(() => { - render(<WorldMap loading={false} />); - }); - it('should render without exploding', () => { - const { container } = render(<WorldMap loading={false} />); - expect(container.firstChild).toBeInTheDocument(); - }); - it('should render the expected container/child element', () => { - expect(screen.getByTestId('worldMap__container')).toBeInTheDocument(); - }); - it('should render the title', () => { - expect(screen.getByText('mix-nodes around the globe')).toBeTruthy(); - }); - it('should render the map/SVG', () => { - expect(screen.getByTestId('svg')).toBeInTheDocument(); - }); - it('should render map at correct size/dims', () => { - const expectedWidth = '1000'; - const expectedHeight = '800'; - expect(screen.getByTestId('svg')).toHaveAttribute('width', expectedWidth); - expect(screen.getByTestId('svg')).toHaveAttribute('height', expectedHeight); - }); -}); diff --git a/explorer/src/tests/todo.test.ts b/explorer/src/tests/todo.test.ts deleted file mode 100644 index 0580a22ccc..0000000000 --- a/explorer/src/tests/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('testing jest config', () => { - test('jest works with typescript', () => { - const foo = () => 42; - - expect(foo()).toBe(42); - }); -}); diff --git a/explorer/src/theme/index.tsx b/explorer/src/theme/index.tsx deleted file mode 100644 index 3542bd9c9a..0000000000 --- a/explorer/src/theme/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { useMainContext } from '../context/main'; - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext(); - return <NymNetworkExplorerThemeProvider mode={mode}>{children}</NymNetworkExplorerThemeProvider>; -}; diff --git a/explorer/src/theme/mui-theme.d.ts b/explorer/src/theme/mui-theme.d.ts deleted file mode 100644 index fc1f856f91..0000000000 --- a/explorer/src/theme/mui-theme.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme {} - interface ThemeOptions extends Partial<NymTheme> {} - interface Palette extends NymPaletteWithExtensions {} - interface PaletteOptions extends NymPaletteWithExtensionsOptions {} -} diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts deleted file mode 100644 index 9319c33952..0000000000 --- a/explorer/src/typeDefs/explorer-api.ts +++ /dev/null @@ -1,277 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -export type GatewayResponse = GatewayBond[]; - -export interface GatewayReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer/src/typeDefs/filters.ts b/explorer/src/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer/src/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer/src/typeDefs/network.ts b/explorer/src/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer/src/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer/src/typeDefs/tables.ts b/explorer/src/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer/src/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer/src/typings/FC.d.ts b/explorer/src/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer/src/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer/src/typings/jpeg.d.ts b/explorer/src/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer/src/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer/src/typings/json.d.ts b/explorer/src/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer/src/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/png.d.ts b/explorer/src/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer/src/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/react-identicons.d.ts b/explorer/src/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer/src/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer/src/typings/react-tooltip.d.ts b/explorer/src/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer/src/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer/src/typings/svg.d.ts b/explorer/src/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer/src/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer/src/utils/currency.ts b/explorer/src/utils/currency.ts deleted file mode 100644 index ad6ed1f165..0000000000 --- a/explorer/src/utils/currency.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - return `${toDisplay(printableAmount, dp)} ${printableDenom}`; -}; - -export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => - printableCoin({ - amount, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts deleted file mode 100644 index 91b082c5ca..0000000000 --- a/explorer/src/utils/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '../typeDefs/explorer-api'; -import { EconomicsRowsType } from '../components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { - const progressBarValue = value?.progressBarValue || 0; - const fieldValue = value.value; - - if (progressBarValue > 100) { - return theme.palette.warning.main; - } - if (field === 'selectionChance') { - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - switch (fieldValue) { - case 'High': - case 'VeryHigh': - return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Good': - case 'Moderate': - return theme.palette.nym.networkExplorer.selectionChance.moderate; - case 'Low': - case 'VeryLow': - return theme.palette.nym.networkExplorer.selectionChance.underModerate; - default: - return theme.palette.nym.wallet.fee; - } - } - return theme.palette.nym.wallet.fee; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer/src/x-data-grid/LICENSE b/explorer/src/x-data-grid/LICENSE deleted file mode 100644 index 126bc57eaa..0000000000 --- a/explorer/src/x-data-grid/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Material-UI SAS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/explorer/src/x-data-grid/README.md b/explorer/src/x-data-grid/README.md deleted file mode 100644 index c163d7ceeb..0000000000 --- a/explorer/src/x-data-grid/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# @mui/x-data-grid - -This package is the community edition of the data grid component. -It's part of Material-UI X, an open core extension of Material-UI, with advanced components. - -## Installation - -Install the package in your project directory with: - -```sh -// with npm -npm install @mui/x-data-grid - -// with yarn -yarn add @mui/x-data-grid -``` - -This component has two peer dependencies that you will need to install as well. - -```json -"peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "react": "^17.0.0" -}, -``` - -## Documentation - -[The documentation](https://material-ui.com/components/data-grid/) diff --git a/explorer/src/x-data-grid/package.json b/explorer/src/x-data-grid/package.json deleted file mode 100644 index 46225fa760..0000000000 --- a/explorer/src/x-data-grid/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "_from": "@mui/x-data-grid", - "_id": "@mui/x-data-grid@4.0.0", - "_inBundle": false, - "_integrity": "sha512-BYn7uLx5tJbMarcWltjjVArWNBdC22/2xOpq3Azhltbb3TRx3h2RLUeKwZI685xmGHmzvtu6QqaoYqQgFe/h+g==", - "_location": "/@mui/x-data-grid", - "_phantomChildren": {}, - "_requested": { - "type": "tag", - "registry": true, - "raw": "@mui/x-data-grid", - "name": "@mui/x-data-grid", - "escapedName": "@mui%2fx-data-grid", - "scope": "@mui", - "rawSpec": "", - "saveSpec": null, - "fetchSpec": "latest" - }, - "_requiredBy": [ - "#USER", - "/" - ], - "_resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-4.0.0.tgz", - "_shasum": "e9e9c33a8b86e85872c48f30f4a8de72ee819153", - "_spec": "@mui/x-data-grid", - "_where": "/Users/adrianthompson/Documents/nym/explorer", - "author": { - "name": "MUI Team" - }, - "bugs": { - "url": "https://github.com/mui-org/material-ui-x/issues" - }, - "bundleDependencies": false, - "dependencies": { - "@material-ui/utils": "^5.0.0-beta.4", - "clsx": "^1.1.1", - "prop-types": "^15.7.2", - "reselect": "^4.0.0" - }, - "deprecated": false, - "description": "The community edition of the data grid component (Material-UI X).", - "engines": { - "node": ">=12.0.0" - }, - "files": [ - "dist/*" - ], - "gitHead": "940d6238fe499f027ee27e818d714f79ca40e9a5", - "homepage": "https://material-ui.com/components/data-grid/", - "keywords": [ - "react", - "react-component", - "material-ui", - "mui", - "react-table", - "table", - "datatable", - "data-table", - "datagrid", - "data-grid" - ], - "license": "MIT", - "main": "dist/index-cjs.js", - "module": "dist/index-esm.js", - "name": "@mui/x-data-grid", - "peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "@mui/styles": "^4.11.4 || ^5.0.0-beta.0", - "react": "^17.0.0" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mui-org/material-ui-x.git", - "directory": "packages/grid/data-grid" - }, - "scripts": { - "build": "cd ../ && rollup --config rollup.data-grid.config.js", - "typescript": "tsc -p tsconfig.json" - }, - "setupFiles": [ - "<rootDir>/src/setupTests.js" - ], - "sideEffects": false, - "types": "dist/data-grid.d.ts", - "version": "4.0.0" -} \ No newline at end of file diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json deleted file mode 100644 index 093340052d..0000000000 --- a/explorer/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../ts-packages/tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx", - "outDir": "./dist", - "module": "CommonJS", - "baseUrl": ".", - "paths": { - "@src/*": ["./src/*"], - "@assets/*": ["../assets/*"] - } - }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] -} diff --git a/explorer/webpack.common.js b/explorer/webpack.common.js deleted file mode 100644 index 7bb9cb9032..0000000000 --- a/explorer/webpack.common.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -const { mergeWithRules } = require('webpack-merge'); -const { webpackCommon } = require('@nymproject/webpack'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(webpackCommon(__dirname), { - entry: path.resolve(__dirname, 'src/index.tsx'), - output: { - path: path.resolve(__dirname, 'dist'), - publicPath: '/', - }, - resolve: { - fallback: { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - buffer: require.resolve('buffer'), - }, - }, -}); diff --git a/explorer/webpack.config.js b/explorer/webpack.config.js deleted file mode 100644 index 550951cb52..0000000000 --- a/explorer/webpack.config.js +++ /dev/null @@ -1,76 +0,0 @@ -const { mergeWithRules } = require('webpack-merge'); -const webpack = require('webpack'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const ReactRefreshTypeScript = require('react-refresh-typescript'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'development', - devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - options: { - getCustomTransformers: () => ({ - before: [ReactRefreshTypeScript()], - }), - // `ts-loader` does not work with HMR unless `transpileOnly` is used. - // If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative. - transpileOnly: true, - }, - }, - ], - }, - plugins: [ - new ReactRefreshWebpackPlugin(), - - // this can be included automatically by the dev server, however build mode fails if missing - new webpack.HotModuleReplacementPlugin(), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - - target: 'web', - - devServer: { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - historyApiFallback: true, - }, - - // recommended for faster rebuild - optimization: { - runtimeChunk: true, - removeAvailableModules: false, - removeEmptyChunks: false, - splitChunks: false, - }, - - cache: { - type: 'filesystem', - buildDependencies: { - // restart on config change - config: ['./webpack.config.js'], - }, - }, -}); diff --git a/explorer/webpack.prod.js b/explorer/webpack.prod.js deleted file mode 100644 index 259c32a4af..0000000000 --- a/explorer/webpack.prod.js +++ /dev/null @@ -1,52 +0,0 @@ -const webpack = require('webpack'); -const { mergeWithRules } = require('webpack-merge'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'production', - - // TODO: no source maps, add back - devtool: false, - - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - optimization: { - minimizer: [`...`, new CssMinimizerPlugin()], - splitChunks: { - chunks: 'all', - }, - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - }), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - output: { - pathinfo: false, - filename: '[name].[contenthash].js', - }, -}); diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index e9d76a1f75..0a487afe86 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -16,10 +16,6 @@ pub(crate) struct Cli { #[clap(long, env = "NETWORK_NAME")] pub(crate) network_name: String, - /// Explorer api url. - #[clap(short, long, env = "EXPLORER_API")] - pub(crate) explorer_api: String, - /// Nym api url. #[clap(short, long, env = "NYM_API")] pub(crate) nym_api: String, diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index 761d08f614..8f1fe1277a 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -34,8 +34,6 @@ pub(crate) fn validators() -> Vec<ValidatorDetails> { )] } -pub(crate) const EXPLORER_API: &str = "https://qa-network-explorer.qa.nymte.ch/api/"; - pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -53,7 +51,6 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, - explorer_api: parse_optional_str(EXPLORER_API), nym_vpn_api_url: None, } } diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index c436c9f09e..cae0140cf8 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -34,8 +34,6 @@ pub(crate) fn validators() -> Vec<ValidatorDetails> { )] } -pub(crate) const EXPLORER_API: &str = "https://sandbox-explorer.nymtech.net/api/"; - pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -53,7 +51,6 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, - explorer_api: parse_optional_str(EXPLORER_API), nym_vpn_api_url: None, } } diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 6c65eda06c..4615be274c 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -81,7 +81,6 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { api_url: None, }], contracts, - explorer_api: None, nym_vpn_api_url: None, } } diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 8e978e4fc6..ec7bf8992a 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -38,7 +38,6 @@ impl InitCtx { network_name: "foomp".to_string(), // does this matter? endpoints: vec![], contracts: Default::default(), - explorer_api: None, nym_vpn_api_url: None, }; Ok(Config::try_from_nym_network_details(&network_details)?) From ac273480f83895b7ed417bf49d14c4cfaa628f0a Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:17:56 +0200 Subject: [PATCH 22/47] Fix CI version check (#5851) * Fix version * Test .rc version * Undo cargo.toml version * Remove comment * Apply to statistics service --- .github/workflows/ci-check-ns-api-version.yml | 6 ++++-- .github/workflows/ci-check-nym-stats-api-version.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 92acb50395..00ef062fc3 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -44,8 +44,10 @@ jobs: echo "Tag is empty" exit 1 fi + # first, list all tags for logging purposes curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq - exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' ) + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) if [[ $exists = "true" ]]; then echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" exit 1 @@ -53,5 +55,5 @@ jobs: echo "Version '$TAG' doesn't exist on the remote" else echo "Unknown output '$exists'" - exit 1 + exit 2 fi diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index 2bcb0bc87e..6f3eb75770 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -44,8 +44,10 @@ jobs: echo "Tag is empty" exit 1 fi + # first, list all tags for logging purposes curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq - exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' ) + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) if [[ $exists = "true" ]]; then echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" exit 1 @@ -53,5 +55,5 @@ jobs: echo "Version '$TAG' doesn't exist on the remote" else echo "Unknown output '$exists'" - exit 1 + exit 2 fi From d6b3d7fc0aa558f8f8eb374c312861011baae19d Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:19:00 +0000 Subject: [PATCH 23/47] [DOCs/operators]: Release notes for v2025.11 cheddar (#5852) * bump up version * add dev features * add operator updates * add updated stats * update prebuild --- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../docs/pages/operators/changelog.mdx | 47 +++++++++++++++++++ .../pages/operators/nodes/nym-node/setup.mdx | 10 ++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index ac97e28533..3ddd5e309f 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Friday, June 6th 2025, 09:33:10 UTC +Thursday, June 12th 2025, 11:03:30 UTC diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index d2a2454361..4ff68694f9 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -47,6 +47,53 @@ This page displays a full list of all the changes during our release cycle from <VarInfo /> +## `v2025.11-cheddar` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar) +- [`nym-node`](nodes/nym-node.mdx) version `1.13.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-06-10T09:41:31.291089877Z +Build Version: 1.13.0 +Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51 +Commit Date: 2025-06-10T11:39:20.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` +### Operators Updates & Tools + +- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance +- **NIP1** will outline the governance process: Its draft will be published soon for discussion +- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable +- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz) +- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!) + +### Features + +- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783) + +- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there + +- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787) + +- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility. + +- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800): + - Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API` + - Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code). + +- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802) + +- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804) + +- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812) + +- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831) + ## `v2025.10-brie` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie) diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 8c2e00052d..f46a126498 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ## Current version ```sh -nym-node +ym-node Binary Name: nym-node -Build Timestamp: 2025-05-27T10:13:18.511075453Z -Build Version: 1.12.0 -Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0 -Commit Date: 2025-05-27T12:11:13.000000000+02:00 +Build Timestamp: 2025-06-10T09:41:31.291089877Z +Build Version: 1.13.0 +Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51 +Commit Date: 2025-06-10T11:39:20.000000000+02:00 Commit Branch: HEAD rustc Version: 1.86.0 rustc Channel: stable From 0caa62796017e6f03d96fde499def7677867a4fa Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov <andrej@nymtech.net> Date: Thu, 12 Jun 2025 15:12:29 +0200 Subject: [PATCH 24/47] Fix missing await on self.close_pool_inner() --- sqlx-pool-guard/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index d9d89eaae7..96f28f2861 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -56,18 +56,16 @@ impl SqlitePoolGuard { // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. if !self.connection_pool.is_closed() { tracing::info!("Closing sqlite pool: {}", self.database_path.display()); - _ = self.close_pool_inner(); + self.close_pool_inner().await.ok(); } } async fn close_pool_inner(&self) -> std::io::Result<()> { self.connection_pool.close().await; - if let Err(e) = self.wait_for_db_files_close().await { + self.wait_for_db_files_close().await.inspect_err(|e| { tracing::error!("Failed to wait for file to close: {e}"); - } - - Ok(()) + }) } /// Returns all database files, including shm and wal files. From 1d7ffc1bb62b6c294de870e55fea48fcd9b9fc08 Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov <andrej@nymtech.net> Date: Thu, 12 Jun 2025 15:39:26 +0200 Subject: [PATCH 25/47] test: remove file after closing for a test --- sqlx-pool-guard/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs index 96f28f2861..3dd15ee6c7 100644 --- a/sqlx-pool-guard/src/lib.rs +++ b/sqlx-pool-guard/src/lib.rs @@ -160,7 +160,7 @@ mod tests { .await .unwrap(); - let guard = SqlitePoolGuard::new(database_path, connection_pool); + let guard = SqlitePoolGuard::new(database_path.clone(), connection_pool); assert!( guard .wait_for_db_files_close() @@ -170,5 +170,6 @@ mod tests { ); assert!(guard.close_pool_inner().await.is_ok()); + tokio::fs::remove_file(database_path).await.unwrap(); } } From fec196c09737bb64b2a185269e444a7db1097bbd Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov <andrej@nymtech.net> Date: Thu, 12 Jun 2025 17:16:02 +0200 Subject: [PATCH 26/47] Use display when printing paths --- common/credential-storage/src/persistent_storage/mod.rs | 4 ++-- common/gateway-stats-storage/src/lib.rs | 4 ++-- common/gateway-storage/src/lib.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 565ab94513..16a1ae9d5a 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -54,8 +54,8 @@ impl PersistentStorage { /// * `database_path`: path to the database. pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); let opts = sqlx::sqlite::SqliteConnectOptions::new() diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 8ad34bbca4..d29de3a1b0 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -33,8 +33,8 @@ impl PersistentStatsStorage { /// * `database_path`: path to the database. pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 2d05d43fe7..ea534b8c1b 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -82,8 +82,8 @@ impl GatewayStorage { message_retrieval_limit: i64, ) -> Result<Self, GatewayStorageError> { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config From 378229b04e941a2bb98896dfe665654ce5ee9b88 Mon Sep 17 00:00:00 2001 From: Jack Wampler <jmwample@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:15:36 -0600 Subject: [PATCH 27/47] HTTP Discovery objects & network defaults (#5814) add extended (optional) fields to the NetworkDiscovery and configure fallback hosts --- common/network-defaults/src/mainnet.rs | 30 ++++++++++++++-- common/network-defaults/src/network.rs | 35 +++++++++++++++++++ .../nym-wallet-types/src/network/sandbox.rs | 2 ++ .../testnet-manager/src/manager/network.rs | 2 ++ .../src/manager/network_init.rs | 2 ++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 2b54d17406..2698162197 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "network")] -use crate::{DenomDetails, ValidatorDetails}; +use crate::{ApiUrlConst, DenomDetails, ValidatorDetails}; pub const NETWORK_NAME: &str = "mainnet"; @@ -29,10 +29,36 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; -pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; +pub const NYM_API: &str = "https://validator.nymtech.net/api/"; +#[cfg(feature = "network")] +pub const NYM_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_API, + front_hosts: None, + }, + ApiUrlConst { + url: "https://nym-fronntdoor.vercel.app/api/", + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nym-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; +#[cfg(feature = "network")] +pub const NYM_VPN_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_VPN_API, + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging pub const EXIT_POLICY_URL: &str = diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 8cebe53340..e2b7ceb822 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -37,6 +37,37 @@ pub struct NymNetworkDetails { pub contracts: NymContracts, pub explorer_api: Option<String>, pub nym_vpn_api_url: Option<String>, + pub nym_api_urls: Option<Vec<ApiUrl>>, + pub nym_vpn_api_urls: Option<Vec<ApiUrl>>, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub struct ApiUrl { + /// Expects a string formatted Url + /// + /// see https://docs.rs/url/latest/url/struct.Url.html + pub url: String, + /// Optional alternative equivalent hostnames. Each entry must parse as valid Host + /// + /// see https://docs.rs/url/latest/url/enum.Host.html + pub front_hosts: Option<Vec<String>>, +} + +pub struct ApiUrlConst<'a> { + pub url: &'a str, + pub front_hosts: Option<&'a [&'a str]>, +} + +impl From<ApiUrlConst<'_>> for ApiUrl { + fn from(value: ApiUrlConst) -> Self { + ApiUrl { + url: value.url.to_string(), + front_hosts: value + .front_hosts + .map(|slice| slice.iter().map(|s| s.to_string()).collect()), + } + } } // by default we assume the same defaults as mainnet, i.e. same prefixes and denoms @@ -67,6 +98,8 @@ impl NymNetworkDetails { contracts: Default::default(), explorer_api: Default::default(), nym_vpn_api_url: Default::default(), + nym_api_urls: Default::default(), + nym_vpn_api_urls: Default::default(), } } @@ -154,6 +187,8 @@ impl NymNetworkDetails { }, explorer_api: parse_optional_str(mainnet::EXPLORER_API), nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), + nym_api_urls: None, + nym_vpn_api_urls: None, } } diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index c436c9f09e..68d389ebfb 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -55,5 +55,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { }, explorer_api: parse_optional_str(EXPLORER_API), nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 6c65eda06c..623a3d0a8e 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -83,6 +83,8 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { contracts, explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } } diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 8e978e4fc6..6deb16046b 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -40,6 +40,8 @@ impl InitCtx { contracts: Default::default(), explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, }; Ok(Config::try_from_nym_network_details(&network_details)?) } From a31597aca9248559559928fca3286959248d4be4 Mon Sep 17 00:00:00 2001 From: Simon Wicky <simon@nymtech.net> Date: Fri, 13 Jun 2025 09:30:00 +0200 Subject: [PATCH 28/47] fix removal of qa env --- nym-wallet/src-tauri/src/config/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 78c214d1d1..548f135173 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -459,7 +459,7 @@ impl fmt::Display for OptionalValidators { .as_ref() .map(|validators| format!(",\nsandbox: [\n{}\n]", validators.iter().format("\n"))) .unwrap_or_default(); - write!(f, "{s1}{s2}{s3}") + write!(f, "{s1}{s2}") } } From 5f9f7f0facf74572d2d606d4307c75d14df4ca7a Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov <andrej@nymtech.net> Date: Fri, 13 Jun 2025 11:00:48 +0200 Subject: [PATCH 29/47] Clear out screaming logs --- common/client-core/surb-storage/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/client-core/surb-storage/src/lib.rs b/common/client-core/surb-storage/src/lib.rs index 9e0ba14b34..b1c0f3088b 100644 --- a/common/client-core/surb-storage/src/lib.rs +++ b/common/client-core/surb-storage/src/lib.rs @@ -33,7 +33,6 @@ where self.backend.load_surb_storage().await } - // this will have to get enabled after merging develop pub async fn flush_on_shutdown( mut self, mem_state: CombinedReplyStorage, @@ -50,7 +49,6 @@ where shutdown.recv().await; info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); - info!("you MUST NOT forcefully shutdown now or you risk data corruption!"); if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { error!("failed to flush our reply-related data to the persistent storage: {err}") } else { From b9339b8f0c54d3ed202b8704c8a0ef9dc0f3e341 Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:19:35 +0200 Subject: [PATCH 30/47] Add /status endpoints (#5857) * Add /status endpoints * Bump package version * pub use instead of import --- Cargo.lock | 2 +- .../nym-node-status-api/Cargo.toml | 4 +- .../nym-node-status-api/src/http/api/mod.rs | 4 +- .../src/http/api/status.rs | 46 +++++++++++++++++++ .../nym-node-status-api/src/http/state.rs | 38 ++++++++++++++- 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 nym-node-status-api/nym-node-status-api/src/http/api/status.rs diff --git a/Cargo.lock b/Cargo.lock index 0aed14970b..53ee2debe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6649,7 +6649,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.0" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 104e4c9863..7e63db3672 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -28,7 +28,7 @@ moka = { workspace = true, features = ["future"] } # Nym API: revert after Cheddar is out nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } -nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } +nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["openapi"]} nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs index 5bc06be2b2..bd4300275b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod metrics; pub(crate) mod mixnodes; pub(crate) mod nym_nodes; pub(crate) mod services; +pub(crate) mod status; pub(crate) mod summary; pub(crate) mod testruns; @@ -39,7 +40,8 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()) - .nest("/metrics", metrics::routes()), + .nest("/metrics", metrics::routes()) + .nest("/status", status::routes()), ) .nest( "/explorer/v3", diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/status.rs b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs new file mode 100644 index 0000000000..306a7f9c2b --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs @@ -0,0 +1,46 @@ +use axum::{extract::State, Json, Router}; +use nym_validator_client::models::BinaryBuildInformationOwned; +use tracing::instrument; + +use crate::http::{ + error::HttpResult, + state::{AppState, HealthInfo}, +}; + +pub(crate) fn routes() -> Router<AppState> { + Router::new() + .route("/build_information", axum::routing::get(build_information)) + .route("/health", axum::routing::get(health)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build_information", + context_path = "/v2/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn build_information( + State(state): State<AppState>, +) -> HttpResult<Json<BinaryBuildInformationOwned>> { + let build_info = state.build_information().to_owned(); + + Ok(Json(build_info)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v2/status", + responses( + (status = 200, body = HealthInfo) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn health(State(state): State<AppState>) -> HttpResult<Json<HealthInfo>> { + Ok(Json(state.health())) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 0a5ef4fca9..8d197f9a07 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -1,15 +1,20 @@ use cosmwasm_std::Decimal; use itertools::Itertools; use moka::{future::Cache, Entry}; +use nym_bin_common::bin_info_owned; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_mixnet_contract_common::NodeId; use nym_validator_client::nym_api::SkimmedNode; use semver::Version; +use serde::Serialize; use std::{collections::HashMap, sync::Arc, time::Duration}; +use time::UtcDateTime; use tokio::sync::RwLock; use tracing::{error, instrument, warn}; +use utoipa::ToSchema; +use super::models::SessionStats; use crate::{ db::{queries, DbPool}, http::models::{ @@ -18,7 +23,7 @@ use crate::{ monitor::{DelegationsCache, NodeGeoCache}, }; -use super::models::SessionStats; +pub(crate) use nym_validator_client::models::BinaryBuildInformationOwned; #[derive(Debug, Clone)] pub(crate) struct AppState { @@ -28,6 +33,7 @@ pub(crate) struct AppState { agent_max_count: i64, node_geocache: NodeGeoCache, node_delegations: Arc<RwLock<DelegationsCache>>, + bin_info: BinaryInfo, } impl AppState { @@ -46,6 +52,7 @@ impl AppState { agent_max_count, node_geocache, node_delegations, + bin_info: BinaryInfo::new(), } } @@ -78,6 +85,15 @@ impl AppState { .await .delegations_owned(node_id) } + + pub(crate) fn health(&self) -> HealthInfo { + let uptime = (UtcDateTime::now() - self.bin_info.startup_time).whole_seconds(); + HealthInfo { uptime } + } + + pub(crate) fn build_information(&self) -> &BinaryBuildInformationOwned { + &self.bin_info.build_info + } } static GATEWAYS_LIST_KEY: &str = "gateways"; @@ -631,3 +647,23 @@ async fn aggregate_node_info_from_db( Ok(parsed_nym_nodes) } + +#[derive(Debug, Clone)] +pub(crate) struct BinaryInfo { + startup_time: UtcDateTime, + build_info: BinaryBuildInformationOwned, +} + +impl BinaryInfo { + fn new() -> Self { + Self { + startup_time: UtcDateTime::now(), + build_info: bin_info_owned!(), + } + } +} + +#[derive(Serialize, ToSchema)] +pub(crate) struct HealthInfo { + uptime: i64, +} From 4d09b6f2e58f08c3f9989c2990cd96e6b428d953 Mon Sep 17 00:00:00 2001 From: Georgio Nicolas <me@georgio.xyz> Date: Wed, 4 Jun 2025 23:21:49 +0200 Subject: [PATCH 31/47] bte/proof_chunking.rs: Check for potential arithmentic overflows --- common/dkg/src/bte/proof_chunking.rs | 103 ++++++++++++++++++++++----- common/dkg/src/error.rs | 6 ++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index 1365aecbd7..d756f08733 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256; /// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS); +const EE: usize = 1 << NUM_CHALLENGE_BITS; // type alias for ease of use type FirstChallenge = Vec<Vec<Vec<u64>>>; @@ -110,21 +111,20 @@ impl ProofOfChunking { // define bounds for the blinding factors let n = instance.public_keys.len(); let m = NUM_CHUNKS; - let ee = 1 << NUM_CHALLENGE_BITS; - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = (2 * (PARALLEL_RUNS as u64)) - .checked_mul(ss) - .expect("overflow in Z = 2 * l * S"); + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S + let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?; let ss_scalar = Scalar::from(ss); // rather than generating blinding factors in [-S, Z-1] directly, // do it via [0, Z - 1 + S + 1] and deal with the shift later. - let combined_upper_range = (zz - 1) - .checked_add(ss + 1) - .expect("overflow in Z - 1 + S + 1"); + // combined_upper_range = Z - 1 + S + 1 + + let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | Z - 1 + S + 1", + })?; let mut betas = Vec::with_capacity(PARALLEL_RUNS); let mut bs = Vec::with_capacity(PARALLEL_RUNS); @@ -178,12 +178,23 @@ impl ProofOfChunking { // I think this part is more readable with a range loop #[allow(clippy::needless_range_loop)] for l in 0..PARALLEL_RUNS { - let mut sum = 0; + let mut sum: u64 = 0; for (i, witness_i) in witnesses_s.iter().enumerate() { for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() { debug_assert!(std::mem::size_of::<Chunk>() <= std::mem::size_of::<u64>()); - sum += first_challenge[i][j][l] * (*witness_ij as u64) + // sum += first_challenge[i][j][l] * (*witness_ij as u64) + sum = sum + .checked_add( + first_challenge[i][j][l] + .checked_mul(*witness_ij as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)", + })?; } } @@ -191,7 +202,18 @@ impl ProofOfChunking { continue 'retry_loop; } // shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range - let response = sum + shifted_blinding_factors[l] - ss; + // let response = sum + shifted_blinding_factors[l] - ss; + let response = sum + .checked_add(shifted_blinding_factors[l]) + .ok_or(DkgError::ArithmeticOverflow { + info: + "ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)", + })? + .checked_sub(ss) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss", + })?; + if response < zz { responses_chunks.push(response) } else { @@ -276,11 +298,14 @@ impl ProofOfChunking { ensure_len!(&self.responses_r, n); ensure_len!(&self.responses_chunks, PARALLEL_RUNS); - let ee = 1 << NUM_CHALLENGE_BITS; + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = 2 * (PARALLEL_RUNS as u64) * ss; + let zz: u64; + match compute_ss_zz(n, m) { + Ok((_, zz_res)) => zz = zz_res, + _ => return false, + }; for response_chunk in &self.responses_chunks { if response_chunk >= &zz { @@ -411,7 +436,7 @@ impl ProofOfChunking { random_oracle_builder.update(lambda_e.to_be_bytes()); let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize()); - let range_max_excl = 1 << NUM_CHALLENGE_BITS; + let range_max_excl = EE as u64; (0..n) .map(|_| { @@ -637,6 +662,50 @@ impl ProofOfChunking { } } +fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> { + // let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; + // CHUNK_MAX corresponds to paper's B + + let ee = EE; + + let ss = n + .checked_mul(m) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | n * m", + })? + .checked_mul( + CHUNK_SIZE + .checked_sub(1) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)", + })? + .checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (ee - 1)", + })?) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs", + })? as u64; + + // let zz = 2 * PARALLEL_RUNS as u64 * ss; + // Z = 2 * l * S + + let zz = 2u64 + .checked_mul(PARALLEL_RUNS as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | 2 * l", + })? + .checked_mul(ss) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (2 * l) * S", + })?; + + Ok((ss, zz)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/dkg/src/error.rs b/common/dkg/src/error.rs index 9329f32794..5bea0fe3a3 100644 --- a/common/dkg/src/error.rs +++ b/common/dkg/src/error.rs @@ -99,6 +99,12 @@ pub enum DkgError { "The reshared dealing has different public constant coefficient than its prior variant" )] InvalidResharing, + + #[error("Arithmetic Overflow: {info}")] + ArithmeticOverflow { info: &'static str }, + + #[error("Arithmetic Underflow: {info}")] + ArithmeticUnderflow { info: &'static str }, } impl DkgError { From e2f2ab89ecc1bb4e496fbf171415de8b36c686de Mon Sep 17 00:00:00 2001 From: Georgio Nicolas <me@georgio.xyz> Date: Wed, 4 Jun 2025 23:33:45 +0200 Subject: [PATCH 32/47] dkg: add CryptoRng trait requirement --- common/dkg/benches/benchmarks.rs | 5 +++-- common/dkg/src/bte/encryption.rs | 8 ++++++-- common/dkg/src/bte/keys.rs | 8 ++++++-- common/dkg/src/bte/proof_chunking.rs | 8 +++++--- common/dkg/src/bte/proof_discrete_log.rs | 7 ++++++- common/dkg/src/bte/proof_sharing.rs | 6 ++++-- common/dkg/src/dealing.rs | 3 ++- common/dkg/src/interpolation/polynomial.rs | 3 ++- 8 files changed, 34 insertions(+), 14 deletions(-) diff --git a/common/dkg/benches/benchmarks.rs b/common/dkg/benches/benchmarks.rs index 2951a57a7c..37217bf45d 100644 --- a/common/dkg/benches/benchmarks.rs +++ b/common/dkg/benches/benchmarks.rs @@ -14,6 +14,7 @@ use nym_dkg::bte::{ }; use nym_dkg::interpolation::polynomial::Polynomial; use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold}; +use rand::CryptoRng; use rand_core::{RngCore, SeedableRng}; use std::collections::BTreeMap; @@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) { } fn prepare_keys( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, nodes: usize, ) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) { let params = setup(); @@ -50,7 +51,7 @@ fn prepare_keys( } fn prepare_resharing( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, params: &Params, nodes: usize, threshold: Threshold, diff --git a/common/dkg/src/bte/encryption.rs b/common/dkg/src/bte/encryption.rs index 8cd6a3e774..56fdd148cd 100644 --- a/common/dkg/src/bte/encryption.rs +++ b/common/dkg/src/bte/encryption.rs @@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share}; use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar}; use ff::Field; use group::{Curve, Group, GroupEncoding}; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::HashMap; use std::ops::Neg; @@ -191,7 +192,7 @@ impl HazmatRandomness { pub fn encrypt_shares( shares: &[(&Share, &PublicKey)], params: &Params, - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> (Ciphertexts, HazmatRandomness) { let g1 = G1Projective::generator(); @@ -574,7 +575,10 @@ mod tests { #[test] fn ciphertexts_roundtrip() { - fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts { + fn random_ciphertexts( + mut rng: impl RngCore + CryptoRng, + num_receivers: usize, + ) -> Ciphertexts { Ciphertexts { rr: (0..NUM_CHUNKS) .map(|_| G1Projective::random(&mut rng)) diff --git a/common/dkg/src/bte/keys.rs b/common/dkg/src/bte/keys.rs index 132f161f5e..20d896a547 100644 --- a/common/dkg/src/bte/keys.rs +++ b/common/dkg/src/bte/keys.rs @@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; // produces public key and a decryption key for the root of the tree -pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { +pub fn keygen( + params: &Params, + mut rng: impl RngCore + CryptoRng, +) -> (DecryptionKey, PublicKeyWithProof) { let g1 = G1Projective::generator(); let g2 = G2Projective::generator(); @@ -244,7 +248,7 @@ pub struct KeyPair { } impl KeyPair { - pub fn new(params: &Params, rng: impl RngCore) -> Self { + pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self { let (dk, pk) = keygen(params, rng); Self { private_key: dk, diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index d756f08733..1021ac9fb4 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder}; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::{Group, GroupEncoding}; -use rand::Rng; +use rand::{CryptoRng, Rng}; use rand_core::{RngCore, SeedableRng}; const CHUNKING_ORACLE_DOMAIN: &[u8] = @@ -95,7 +95,7 @@ impl ProofOfChunking { // Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to // what we wanted. pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &[Scalar; NUM_CHUNKS], witnesses_s: &[Share], @@ -721,7 +721,9 @@ mod tests { ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>, } - fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) { + fn setup( + mut rng: impl RngCore + CryptoRng, + ) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) { let g1 = G1Projective::generator(); let mut pks = Vec::with_capacity(NODES); diff --git a/common/dkg/src/bte/proof_discrete_log.rs b/common/dkg/src/bte/proof_discrete_log.rs index 95eef6eb42..3956e3837d 100644 --- a/common/dkg/src/bte/proof_discrete_log.rs +++ b/common/dkg/src/bte/proof_discrete_log.rs @@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; @@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog { } impl ProofOfDiscreteLog { - pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self { + pub fn construct( + mut rng: impl RngCore + CryptoRng, + public: &G1Projective, + witness: &Scalar, + ) -> Self { let mut rand_x = Scalar::random(&mut rng); let rand_commitment = G1Projective::generator() * rand_x; let challenge = Self::compute_challenge(public, &rand_commitment); diff --git a/common/dkg/src/bte/proof_sharing.rs b/common/dkg/src/bte/proof_sharing.rs index 4b843bb8f4..fe088c772f 100644 --- a/common/dkg/src/bte/proof_sharing.rs +++ b/common/dkg/src/bte/proof_sharing.rs @@ -9,6 +9,7 @@ use crate::{NodeIndex, Share}; use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; @@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing { impl ProofOfSecretSharing { pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &Scalar, witnesses_s: &[Share], @@ -309,13 +310,14 @@ mod tests { use super::*; use crate::interpolation::polynomial::Polynomial; use group::Group; + use rand::CryptoRng; use rand_core::SeedableRng; const NODES: u64 = 50; const THRESHOLD: u64 = 40; fn setup( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> ( BTreeMap<NodeIndex, PublicKey>, PublicCoefficients, diff --git a/common/dkg/src/dealing.rs b/common/dkg/src/dealing.rs index a051f9f88c..fac5f2a9e6 100644 --- a/common/dkg/src/dealing.rs +++ b/common/dkg/src/dealing.rs @@ -13,6 +13,7 @@ use crate::utils::deserialize_g2; use crate::{NodeIndex, Share, Threshold}; use bls12_381::{G2Projective, Scalar}; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; @@ -94,7 +95,7 @@ impl Dealing { // I'm not a big fan of this function signature, but I'm not clear on how to improve it while // allowing the dealer to skip decryption of its own share if it was also one of the receivers pub fn create( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng + CryptoRng, params: &Params, dealer_index: NodeIndex, threshold: Threshold, diff --git a/common/dkg/src/interpolation/polynomial.rs b/common/dkg/src/interpolation/polynomial.rs index 671b8e6832..f3874806e1 100644 --- a/common/dkg/src/interpolation/polynomial.rs +++ b/common/dkg/src/interpolation/polynomial.rs @@ -6,6 +6,7 @@ use crate::utils::deserialize_g2; use bls12_381::{G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::ops::{Add, Index, IndexMut}; use zeroize::Zeroize; @@ -120,7 +121,7 @@ impl Polynomial { // for polynomial of degree n, we generate n+1 values // (for example for degree 1, like y = x + 2, we need [2,1]) /// Creates new pseudorandom polynomial of specified degree. - pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self { + pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self { Polynomial { coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(), } From 56aad75220c674bd5ad5c9f68659c247c46eb210 Mon Sep 17 00:00:00 2001 From: Georgio Nicolas <me@georgio.xyz> Date: Wed, 4 Jun 2025 23:59:20 +0200 Subject: [PATCH 33/47] dkg: verify integrity of ciphertexts during decryption --- common/dkg/benches/benchmarks.rs | 16 ++++++--- common/dkg/src/bte/encryption.rs | 45 ++++++++++++++++++++----- common/dkg/src/dealing.rs | 4 +-- common/dkg/tests/integration.rs | 15 +++++---- nym-api/src/ecash/dkg/key_derivation.rs | 8 ++++- 5 files changed, 65 insertions(+), 23 deletions(-) diff --git a/common/dkg/benches/benchmarks.rs b/common/dkg/benches/benchmarks.rs index 37217bf45d..d12305bba7 100644 --- a/common/dkg/benches/benchmarks.rs +++ b/common/dkg/benches/benchmarks.rs @@ -69,7 +69,7 @@ fn prepare_resharing( for (i, ref mut dk) in dks.iter_mut().enumerate() { let shares = first_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -155,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -238,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -321,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -548,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) { let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng); c.bench_function("single share decryption", |b| { - b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None))) + b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None))) }); } diff --git a/common/dkg/src/bte/encryption.rs b/common/dkg/src/bte/encryption.rs index 56fdd148cd..300a72441f 100644 --- a/common/dkg/src/bte/encryption.rs +++ b/common/dkg/src/bte/encryption.rs @@ -263,6 +263,7 @@ pub fn encrypt_shares( } pub fn decrypt_share( + params: &Params, dk: &DecryptionKey, // in the case of multiple receivers, specifies which index of ciphertext chunks should be used i: usize, @@ -271,6 +272,10 @@ pub fn decrypt_share( ) -> Result<Share, DkgError> { let mut plaintext = ChunkedShare::default(); + if !ciphertext.verify_integrity(¶ms) { + return Err(DkgError::FailedCiphertextIntegrityCheck); + } + if i >= ciphertext.ciphertext_chunks.len() { return Err(DkgError::UnavailableCiphertext(i)); } @@ -462,10 +467,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -491,10 +508,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } diff --git a/common/dkg/src/dealing.rs b/common/dkg/src/dealing.rs index fac5f2a9e6..e9a96886c2 100644 --- a/common/dkg/src/dealing.rs +++ b/common/dkg/src/dealing.rs @@ -485,7 +485,7 @@ mod tests { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(), @@ -594,7 +594,7 @@ mod tests { for (i, (dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &dealer_indices).unwrap(); diff --git a/common/dkg/tests/integration.rs b/common/dkg/tests/integration.rs index 81c861425d..e34195488e 100644 --- a/common/dkg/tests/integration.rs +++ b/common/dkg/tests/integration.rs @@ -53,11 +53,12 @@ fn single_sender() { // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) for (i, (ref dk, _)) in full_keys.iter().enumerate() { - let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap(); + let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap(); } // and for good measure, check that the dealer's share matches decryption result - let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); + let recovered_dealer = + decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); assert_eq!(recovered_dealer, dealer_share.unwrap()); } @@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); // we know dealer_share matches, but it would be inconvenient to try to put them in here, @@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &node_indices).unwrap(); diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 41cfd11d36..1dc0b0908e 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -399,7 +399,13 @@ impl<R: RngCore + CryptoRng> DkgController<R> { for (dealer_index, dealing) in dealings.into_iter() { // attempt to decrypt our portion let dk = self.state.dkg_keypair().private_key(); - let share = match decrypt_share(dk, receiver_index, &dealing.ciphertexts, None) { + let share = match decrypt_share( + dkg::params(), + dk, + receiver_index, + &dealing.ciphertexts, + None, + ) { Ok(share) => share, Err(err) => { error!("failed to decrypt share {human_index}/{total} generated from dealer {dealer_index}: {err} - can't generate the full key"); From a7cd8efc04f371b9dd66843a3a6299757ae72b36 Mon Sep 17 00:00:00 2001 From: Georgio Nicolas <me@georgio.xyz> Date: Tue, 17 Jun 2025 16:37:50 +0200 Subject: [PATCH 34/47] dkg: fix clippy suggestions --- common/dkg/src/bte/encryption.rs | 2 +- common/dkg/src/bte/proof_chunking.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/common/dkg/src/bte/encryption.rs b/common/dkg/src/bte/encryption.rs index 300a72441f..81b304bc7b 100644 --- a/common/dkg/src/bte/encryption.rs +++ b/common/dkg/src/bte/encryption.rs @@ -272,7 +272,7 @@ pub fn decrypt_share( ) -> Result<Share, DkgError> { let mut plaintext = ChunkedShare::default(); - if !ciphertext.verify_integrity(¶ms) { + if !ciphertext.verify_integrity(params) { return Err(DkgError::FailedCiphertextIntegrityCheck); } diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index 1021ac9fb4..2da6e83b33 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -301,9 +301,8 @@ impl ProofOfChunking { // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) // Z = 2 * l * S - let zz: u64; - match compute_ss_zz(n, m) { - Ok((_, zz_res)) => zz = zz_res, + let zz: u64 = match compute_ss_zz(n, m) { + Ok((_, zz_res)) => zz_res, _ => return false, }; From 6de0c4ce92ccd4207b9f3102389da00816e3d2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Fri, 20 Jun 2025 09:06:56 +0100 Subject: [PATCH 35/47] feat: initial performance contract (#5833) * initialised basic structure for the performance contract * shared code for contract testing * unified common testing methods between performance and nym pool contracts * impl of ExecuteMsg for the contract * impl of QueryMsg for the contract * setting initial authorised NMs during instantiation * additional tests and fixes * ibid * scaffolding for client traits * completed client traits * clippy * naive add performance contract to testnet manager * placeholder values for the performance contract address * introduced admin messages to purge old measurements from the storage * introduced check ensuring performance data is only added to bonded nodes --- .../ci-contracts-upload-binaries.yml | 1 + Cargo.lock | 50 +- Cargo.toml | 8 +- Makefile | 2 +- .../client-libs/validator-client/Cargo.toml | 1 + .../src/nyxd/contract_traits/mod.rs | 10 + .../performance_query_client.rs | 265 ++ .../performance_signing_client.rs | 217 ++ .../src/nyxd/cosmwasm_client/helpers.rs | 7 + .../validator-client/src/nyxd/mod.rs | 4 + .../contracts-common-testing/Cargo.toml | 24 + .../contracts-common-testing/src/helpers.rs | 127 + .../contracts-common-testing/src/lib.rs | 13 + .../src/tester/basic_traits.rs | 239 ++ .../src/tester/extensions.rs | 305 ++ .../src/tester/mod.rs | 276 ++ .../src/tester/storage_wrapper.rs | 20 +- .../contracts-common/Cargo.toml | 1 + .../contracts-common/src/types.rs | 78 +- .../mixnet-contract/Cargo.toml | 1 - .../mixnet-contract/src/gateway.rs | 4 +- .../mixnet-contract/src/mixnode.rs | 6 +- .../mixnet-contract/src/reward_params.rs | 6 +- .../nym-performance-contract/Cargo.toml | 29 + .../nym-performance-contract/src/constants.rs | 13 + .../nym-performance-contract/src/error.rs | 39 + .../nym-performance-contract/src/helpers.rs | 2 + .../nym-performance-contract/src/lib.rs | 12 + .../nym-performance-contract/src/msg.rs | 121 + .../nym-performance-contract/src/types.rs | 242 ++ common/network-defaults/src/mainnet.rs | 5 + common/network-defaults/src/network.rs | 5 + contracts/Cargo.lock | 57 +- contracts/Cargo.toml | 3 +- contracts/mixnet/Cargo.toml | 18 +- contracts/mixnet/src/contract.rs | 3 +- contracts/mixnet/src/lib.rs | 3 + .../mixnet_contract_settings/transactions.rs | 14 +- contracts/mixnet/src/support/tests/mod.rs | 60 +- .../mixnet/src/testable_mixnet_contract.rs | 89 + contracts/nym-pool/Cargo.toml | 6 +- contracts/nym-pool/src/contract.rs | 2 +- contracts/nym-pool/src/helpers.rs | 5 +- contracts/nym-pool/src/queries.rs | 61 +- contracts/nym-pool/src/storage.rs | 104 +- contracts/nym-pool/src/testing/mod.rs | 294 +- contracts/nym-pool/src/transactions.rs | 92 +- contracts/performance/.cargo/config | 4 + contracts/performance/Cargo.toml | 42 + contracts/performance/Makefile | 5 + contracts/performance/src/bin/schema.rs | 14 + contracts/performance/src/contract.rs | 187 ++ contracts/performance/src/helpers.rs | 118 + contracts/performance/src/lib.rs | 13 + contracts/performance/src/queries.rs | 588 ++++ .../performance/src/queued_migrations.rs | 2 + contracts/performance/src/storage.rs | 2603 +++++++++++++++++ contracts/performance/src/testing/mod.rs | 606 ++++ contracts/performance/src/transactions.rs | 305 ++ nym-wallet/Cargo.lock | 15 +- .../nym-wallet-types/src/network/sandbox.rs | 5 + tools/internal/testnet-manager/Cargo.toml | 1 + .../migrations/02_performance_contract.sql | 53 + .../testnet-manager/src/manager/contract.rs | 11 +- .../testnet-manager/src/manager/network.rs | 4 + .../src/manager/network_init.rs | 47 + .../src/manager/storage/manager.rs | 44 +- .../src/manager/storage/mod.rs | 47 +- .../src/manager/storage/models.rs | 9 + 69 files changed, 7214 insertions(+), 453 deletions(-) create mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs create mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs rename contracts/nym-pool/src/testing/storage.rs => common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs (89%) create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs create mode 100644 common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs create mode 100644 contracts/mixnet/src/testable_mixnet_contract.rs create mode 100644 contracts/performance/.cargo/config create mode 100644 contracts/performance/Cargo.toml create mode 100644 contracts/performance/Makefile create mode 100644 contracts/performance/src/bin/schema.rs create mode 100644 contracts/performance/src/contract.rs create mode 100644 contracts/performance/src/helpers.rs create mode 100644 contracts/performance/src/lib.rs create mode 100644 contracts/performance/src/queries.rs create mode 100644 contracts/performance/src/queued_migrations.rs create mode 100644 contracts/performance/src/storage.rs create mode 100644 contracts/performance/src/testing/mod.rs create mode 100644 contracts/performance/src/transactions.rs create mode 100644 tools/internal/testnet-manager/migrations/02_performance_contract.sql diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index c85d9046db..6cd620bcb1 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -57,6 +57,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/Cargo.lock b/Cargo.lock index d63954e6dc..665a6a4dd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,6 +1923,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cw-multi-test" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "itertools 0.14.0", + "prost 0.13.5", + "schemars", + "serde", + "sha2 0.10.9", + "thiserror 2.0.12", +] + [[package]] name = "cw-storage-plus" version = "2.0.0" @@ -5451,6 +5471,7 @@ dependencies = [ name = "nym-contracts-common" version = "0.5.0" dependencies = [ + "anyhow", "bs58", "cosmwasm-schema", "cosmwasm-std", @@ -5478,6 +5499,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", +] + [[package]] name = "nym-cpp-ffi" version = "0.1.2" @@ -6288,7 +6322,6 @@ dependencies = [ "schemars", "semver 1.0.26", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -6893,6 +6926,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common 0.5.0", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-pool-contract-common" version = "0.1.0" @@ -7712,6 +7758,7 @@ dependencies = [ "nym-mixnet-contract-common 0.6.0", "nym-multisig-contract-common 0.1.0", "nym-network-defaults 0.1.0", + "nym-performance-contract-common", "nym-serde-helpers 0.1.0", "nym-vesting-contract-common 0.7.0", "prost 0.13.5", @@ -10757,6 +10804,7 @@ dependencies = [ "nym-mixnet-contract-common 0.6.0", "nym-multisig-contract-common 0.1.0", "nym-pemstore 0.3.0", + "nym-performance-contract-common", "nym-validator-client 0.1.0", "nym-vesting-contract-common 0.7.0", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index 1c7865562d..8b3404c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,12 @@ members = [ "common/config", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/contracts-common-testing", "common/cosmwasm-smart-contracts/easy_addr", "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", - "common/cosmwasm-smart-contracts/multisig-contract", + "common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract", "common/cosmwasm-smart-contracts/nym-pool-contract", "common/cosmwasm-smart-contracts/vesting-contract", "common/credential-storage", @@ -136,7 +137,6 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", @@ -370,9 +370,6 @@ subtle = "2.5.0" # cosmwasm-related cosmwasm-schema = "=2.2.2" cosmwasm-std = "=2.2.2" -# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1 -# (and ideally we don't want to pull the same dependency twice) -serde-json-wasm = "=1.0.1" # same version as used by cosmwasm cw-utils = "=2.0.0" cw-storage-plus = "=2.0.0" @@ -380,6 +377,7 @@ cw2 = { version = "=2.0.0" } cw3 = { version = "=2.0.0" } cw4 = { version = "=2.0.0" } cw-controllers = { version = "=2.0.0" } +cw-multi-test = "=2.3.2" # cosmrs-related bip32 = { version = "0.5.3", default-features = false } diff --git a/Makefile b/Makefile index 709126a6bb..0bd2234edb 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ clippy: sdk-wasm-lint # Build contracts ready for deploy # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract +CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 7ff310b039..ea4e8beeb4 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } +nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" } nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index 8210c5b476..ad7db0991b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -13,6 +13,7 @@ pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; +pub mod performance_query_client; pub mod vesting_query_client; // signing clients @@ -21,6 +22,7 @@ pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; +pub mod performance_signing_client; pub mod vesting_signing_client; // re-export query traits @@ -29,6 +31,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; +pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits @@ -37,6 +40,7 @@ pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; +pub use performance_signing_client::PerformanceSigningClient; pub use vesting_signing_client::VestingSigningClient; // helper for providing blanket implementation for query clients @@ -44,6 +48,7 @@ pub trait NymContractsProvider { // main fn mixnet_contract_address(&self) -> Option<&AccountId>; fn vesting_contract_address(&self) -> Option<&AccountId>; + fn performance_contract_address(&self) -> Option<&AccountId>; // coconut-related fn ecash_contract_address(&self) -> Option<&AccountId>; @@ -56,6 +61,7 @@ pub trait NymContractsProvider { pub struct TypedNymContracts { pub mixnet_contract_address: Option<AccountId>, pub vesting_contract_address: Option<AccountId>, + pub performance_contract_address: Option<AccountId>, pub ecash_contract_address: Option<AccountId>, pub group_contract_address: Option<AccountId>, @@ -76,6 +82,10 @@ impl TryFrom<NymContracts> for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, + performance_contract_address: value + .performance_contract_address + .map(|addr| addr.parse()) + .transpose()?, ecash_contract_address: value .ecash_contract_address .map(|addr| addr.parse()) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs new file mode 100644 index 0000000000..722a2d002c --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs @@ -0,0 +1,265 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmrs::AccountId; +pub use nym_performance_contract_common::{ + msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, +}; +use nym_performance_contract_common::{ + EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, + NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse, + NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor, + RetiredNetworkMonitorsPagedResponse, +}; +use serde::Deserialize; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceQueryClient { + async fn query_performance_contract<T>( + &self, + query: PerformanceQueryMsg, + ) -> Result<T, NyxdError> + where + for<'a> T: Deserialize<'a>; + + async fn admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::Admin {}) + .await + } + + async fn get_node_performance( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<NodePerformanceResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id }) + .await + } + + async fn get_node_performance_paged( + &self, + node_id: NodeId, + start_after: Option<EpochId>, + limit: Option<u32>, + ) -> Result<NodePerformancePagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + }) + .await + } + + async fn get_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<NodeMeasurementsResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id }) + .await + } + + async fn get_epoch_measurements_paged( + &self, + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, + ) -> Result<EpochMeasurementsPagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_epoch_performance_paged( + &self, + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, + ) -> Result<EpochPerformancePagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_full_historical_performance_paged( + &self, + start_after: Option<(EpochId, NodeId)>, + limit: Option<u32>, + ) -> Result<FullHistoricalPerformancePagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged { + start_after, + limit, + }) + .await + } + + async fn get_network_monitor( + &self, + address: &AccountId, + ) -> Result<NetworkMonitorResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor { + address: address.to_string(), + }) + .await + } + + async fn get_network_monitors_paged( + &self, + start_after: Option<String>, + limit: Option<u32>, + ) -> Result<NetworkMonitorsPagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged { + start_after, + limit, + }) + .await + } + + async fn get_retired_network_monitors_paged( + &self, + start_after: Option<String>, + limit: Option<u32>, + ) -> Result<RetiredNetworkMonitorsPagedResponse, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged { + start_after, + limit, + }) + .await + } +} + +// extension trait to the query client to deal with the paged queries +// (it didn't feel appropriate to combine it with the existing trait +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedPerformanceQueryClient: PerformanceQueryClient { + async fn get_all_node_performance( + &self, + node_id: NodeId, + ) -> Result<Vec<EpochNodePerformance>, NyxdError> { + collect_paged!(self, get_node_performance_paged, performance, node_id) + } + + async fn get_all_epoch_measurements( + &self, + node_id: NodeId, + ) -> Result<Vec<NodeMeasurement>, NyxdError> { + collect_paged!(self, get_epoch_measurements_paged, measurements, node_id) + } + + async fn get_all_epoch_performance( + &self, + epoch_id: EpochId, + ) -> Result<Vec<NodePerformance>, NyxdError> { + collect_paged!(self, get_epoch_performance_paged, performance, epoch_id) + } + + async fn get_all_full_historical_performance( + &self, + ) -> Result<Vec<HistoricalPerformance>, NyxdError> { + collect_paged!(self, get_full_historical_performance_paged, performance) + } + + async fn get_all_network_monitors(&self) -> Result<Vec<NetworkMonitorInformation>, NyxdError> { + collect_paged!(self, get_network_monitors_paged, info) + } + + async fn get_all_retired_network_monitors( + &self, + ) -> Result<Vec<RetiredNetworkMonitor>, NyxdError> { + collect_paged!(self, get_retired_network_monitors_paged, info) + } +} + +#[async_trait] +impl<T> PagedPerformanceQueryClient for T where T: PerformanceQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl<C> PerformanceQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_performance_contract<T>( + &self, + query: PerformanceQueryMsg, + ) -> Result<T, NyxdError> + where + for<'a> T: Deserialize<'a>, + { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + self.query_contract_smart(performance_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered<C: PerformanceQueryClient + Send + Sync>( + client: C, + msg: PerformanceQueryMsg, + ) { + match msg { + PerformanceQueryMsg::Admin {} => client.admin().ignore(), + PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => { + client.get_node_performance(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => client + .get_node_performance_paged(node_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => { + client.get_node_measurements(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_measurements_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_performance_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client + .get_full_historical_performance_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::NetworkMonitor { address } => client + .get_network_monitor(&address.parse().unwrap()) + .ignore(), + PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client + .get_network_monitors_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client + .get_retired_network_monitors_paged(start_after, limit) + .ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs new file mode 100644 index 0000000000..78b960265a --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -0,0 +1,217 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::coin::Coin; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::cosmwasm_client::ContractResponseData; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_performance_contract_common::{ + EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance, + RemoveEpochMeasurementsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceSigningClient { + async fn execute_performance_contract( + &self, + fee: Option<Fee>, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec<Coin>, + ) -> Result<ExecuteResult, NyxdError>; + + async fn update_admin( + &self, + admin: String, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::UpdateAdmin { admin }, + "PerformanceContract::UpdateAdmin".to_string(), + vec![], + ) + .await + } + + async fn submit_performance( + &self, + epoch: EpochId, + data: NodePerformance, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::Submit { epoch, data }, + "PerformanceContract::Submit".to_string(), + vec![], + ) + .await + } + + async fn batch_submit_performance( + &self, + epoch: EpochId, + data: Vec<NodePerformance>, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::BatchSubmit { epoch, data }, + "PerformanceContract::BatchSubmit".to_string(), + vec![], + ) + .await + } + + async fn authorise_network_monitor( + &self, + address: String, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address }, + "PerformanceContract::AuthoriseNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn retire_network_monitor( + &self, + address: String, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RetireNetworkMonitor { address }, + "PerformanceContract::RetireNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn remove_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id }, + "PerformanceContract::RemoveNodeMeasurements".to_string(), + vec![], + ) + .await + } + + async fn partial_remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option<Fee>, + ) -> Result<ExecuteResult, NyxdError> { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id }, + "PerformanceContract::RemoveEpochMeasurements".to_string(), + vec![], + ) + .await + } + + async fn remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option<Fee>, + ) -> Result<(), NyxdError> { + loop { + let execute_res = self + .partial_remove_epoch_measurements(epoch_id, fee.clone()) + .await?; + let response = execute_res + .parse_singleton_json_contract_response::<RemoveEpochMeasurementsResponse>()?; + if !response.additional_entries_to_remove_remaining { + break; + } + } + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl<C> PerformanceSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<<Self as OfflineSigner>::Error>, +{ + async fn execute_performance_contract( + &self, + fee: Option<Fee>, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec<Coin>, + ) -> Result<ExecuteResult, NyxdError> { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + + let signer_address = &self.signer_addresses()?[0]; + self.execute( + signer_address, + performance_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered<C: PerformanceSigningClient + Send + Sync>( + client: C, + msg: PerformanceExecuteMsg, + ) { + match msg { + PerformanceExecuteMsg::UpdateAdmin { admin } => { + client.update_admin(admin, None).ignore() + } + PerformanceExecuteMsg::Submit { epoch, data } => { + client.submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::BatchSubmit { epoch, data } => { + client.batch_submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => { + client.authorise_network_monitor(address, None).ignore() + } + PerformanceExecuteMsg::RetireNetworkMonitor { address } => { + client.retire_network_monitor(address, None).ignore() + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client + .remove_node_measurements(epoch_id, node_id, None) + .ignore(), + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client + .partial_remove_epoch_measurements(epoch_id, None) + .ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 0c4c6c8dcb..e948dc9812 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast; use tracing::error; pub use cosmrs::abci::MsgResponse; +use cosmwasm_std::from_json; +use serde::de::DeserializeOwned; pub fn parse_singleton_u32_from_contract_response(b: Vec<u8>) -> Result<u32, NyxdError> { if b.len() != 4 { @@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> { // requires there's a single response message pub trait ContractResponseData: Sized { + fn parse_singleton_json_contract_response<T: DeserializeOwned>(&self) -> Result<T, NyxdError> { + let b = self.to_singleton_contract_data()?; + from_json(&b).map_err(|err| err.into()) + } + fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> { let b = self.to_singleton_contract_data()?; parse_singleton_u32_from_contract_response(b) diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 5f7cbb8e7c..80dcf01fb0 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -276,6 +276,10 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> { self.config.contracts.vesting_contract_address.as_ref() } + fn performance_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.performance_contract_address.as_ref() + } + fn ecash_contract_address(&self) -> Option<&AccountId> { self.config.contracts.ecash_contract_address.as_ref() } diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml new file mode 100644 index 0000000000..212aa2d373 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nym-contracts-common-testing" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } + +serde = { workspace = true } +rand_chacha = { workspace = true } +rand = { workspace = true } +cw-multi-test = { workspace = true } + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs new file mode 100644 index 0000000000..fa49db306d --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs @@ -0,0 +1,127 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{ + coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps, + Response, StdResult, Storage, +}; +use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey}; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub const TEST_DENOM: &str = "unym"; +pub const TEST_PREFIX: &str = "n"; + +pub fn mock_api() -> MockApi { + MockApi::default().with_prefix(TEST_PREFIX) +} + +pub fn mock_dependencies() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> { + OwnedDeps { + storage: MockStorage::default(), + api: mock_api(), + querier: MockQuerier::default(), + custom_query_type: Default::default(), + } +} + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) +} + +pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> { + let mut deps = mock_dependencies(); + deps.querier = MockQuerier::<Empty>::new(&[( + env.contract.address.as_str(), + coins(100000000000, TEST_DENOM).as_slice(), + )]); + deps +} + +pub fn generate_sorted_addresses(n: usize) -> Vec<Addr> { + let mut rng = test_rng(); + let mut addrs = Vec::with_capacity(n); + for i in 0..n { + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); + } + addrs.sort(); + addrs +} + +pub fn addr<S: AsRef<str>>(raw: S) -> Addr { + mock_api().addr_make(raw.as_ref()) +} + +pub fn sender<S: AsRef<str>>(raw: S) -> MessageInfo { + message_info(&addr(raw), &[]) +} + +pub trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option<BankMsg>; +} + +impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option<BankMsg> { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } +} + +pub trait FullReader<'a> { + type Key; + type Value: Serialize + DeserializeOwned; + + fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>>; + + fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>>; +} + +impl<'a, K, T> FullReader<'a> for Map<K, T> +where + T: Serialize + DeserializeOwned, + K: PrimaryKey<'a> + KeyDeserialize, + K::Output: 'static, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> { + self.range(store, None, None, Order::Ascending).collect() + } +} + +impl<'a, K, T, B> FullReader<'a> for Prefix<K, T, B> +where + K: KeyDeserialize + 'static, + T: Serialize + DeserializeOwned, + B: PrimaryKey<'a>, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> { + self.range(store, None, None, Order::Ascending).collect() + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs new file mode 100644 index 0000000000..acb4292d86 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +// those are all used exclusively for testing thus unwraps, et al. are allowed +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +#![allow(clippy::panic)] + +pub mod helpers; +pub mod tester; + +pub use helpers::*; +pub use tester::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs new file mode 100644 index 0000000000..79b7607e4e --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs @@ -0,0 +1,239 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractTester, TestableNymContract}; +use cosmwasm_std::testing::{message_info, mock_env}; +use cosmwasm_std::{ + from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + Storage, Timestamp, +}; +use cw_multi_test::{next_block, AppResponse, Executor}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::fmt::Debug; + +pub trait ContractOpts { + type ExecuteMsg; + type QueryMsg; + type ContractError; + + fn deps(&self) -> Deps<'_>; + + fn deps_mut(&mut self) -> DepsMut<'_>; + + fn env(&self) -> Env; + + fn addr_make(&self, input: &str) -> Addr; + + fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) { + let env = self.env().clone(); + (self.deps_mut(), env) + } + + fn storage(&self) -> &dyn Storage; + + fn storage_mut(&mut self) -> &mut dyn Storage; + + fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>; + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn unchecked_read_from_contract_storage<T: DeserializeOwned>( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::<T>(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } + + fn execute_raw( + &mut self, + sender: Addr, + message: Self::ExecuteMsg, + ) -> Result<Response, Self::ContractError> { + self.execute_raw_with_balance(sender, &[], message) + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: Self::ExecuteMsg, + ) -> Result<Response, Self::ContractError>; +} + +impl<C> ContractOpts for ContractTester<C> +where + C: TestableNymContract, +{ + type ExecuteMsg = C::ExecuteMsg; + type QueryMsg = C::QueryMsg; + type ContractError = C::ContractError; + + fn deps(&self) -> Deps<'_> { + Deps { + storage: &self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn deps_mut(&mut self) -> DepsMut<'_> { + DepsMut { + storage: &mut self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn env(&self) -> Env { + Env { + block: self.app.block_info(), + contract: ContractInfo { + address: self.contract_address.clone(), + }, + ..mock_env() + } + } + + fn addr_make(&self, input: &str) -> Addr { + self.app.api().addr_make(input) + } + + fn storage(&self) -> &dyn Storage { + &self.storage + } + + fn storage_mut(&mut self) -> &mut dyn Storage { + &mut self.storage + } + + fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> { + let raw = self.deps().storage.get(key.as_ref())?; + from_json(&raw).ok() + } + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + self.deps_mut().storage.set(key.as_ref(), value.as_ref()); + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: C::ExecuteMsg, + ) -> Result<Response, C::ContractError> { + let env = self.env(); + let info = message_info(&sender, coins); + + C::execute()(self.deps_mut(), env, info, message) + } +} + +pub trait ChainOpts: ContractOpts { + fn set_contract_balance(&mut self, balance: Coin); + + fn next_block(&mut self); + + fn set_block_time(&mut self, time: Timestamp); + + fn execute_msg( + &mut self, + sender: Addr, + message: &Self::ExecuteMsg, + ) -> anyhow::Result<AppResponse> { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &Self::ExecuteMsg, + ) -> anyhow::Result<AppResponse>; + + fn execute_arbitrary_contract<T: Serialize + Debug>( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result<AppResponse>; + + fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>( + &self, + contract: Addr, + message: &Q, + ) -> StdResult<T>; + + fn query<T: DeserializeOwned>(&self, message: &Self::QueryMsg) -> StdResult<T>; +} + +impl<C> ChainOpts for ContractTester<C> +where + C: TestableNymContract, +{ + fn set_contract_balance(&mut self, balance: Coin) { + let contract_address = &self.contract_address; + self.app + .router() + .bank + .init_balance( + &mut self.storage.inner_storage(), + contract_address, + vec![balance], + ) + .unwrap(); + } + fn next_block(&mut self) { + self.app.update_block(next_block) + } + + fn set_block_time(&mut self, time: Timestamp) { + self.app.update_block(|b| b.time = time) + } + + fn execute_msg( + &mut self, + sender: Addr, + message: &C::ExecuteMsg, + ) -> anyhow::Result<AppResponse> { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &C::ExecuteMsg, + ) -> anyhow::Result<AppResponse> { + self.app + .execute_contract(sender, self.contract_address.clone(), message, coins) + } + + fn execute_arbitrary_contract<T: Serialize + Debug>( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result<AppResponse> { + let coins = &sender.funds; + let sender = sender.sender; + self.app.execute_contract(sender, contract, message, coins) + } + + fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>( + &self, + contract: Addr, + message: &Q, + ) -> StdResult<T> { + self.app.wrap().query_wasm_smart(contract, message) + } + + fn query<T: DeserializeOwned>(&self, message: &C::QueryMsg) -> StdResult<T> { + self.app + .wrap() + .query_wasm_smart(self.contract_address.as_str(), message) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs new file mode 100644 index 0000000000..75b226bd69 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs @@ -0,0 +1,305 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract, + TEST_DENOM, +}; +use cosmwasm_std::testing::message_info; +use cosmwasm_std::{ + coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage, +}; +use cw_multi_test::Executor; +use cw_storage_plus::{Key, Path, PrimaryKey}; +use rand::RngCore; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::ops::Deref; + +pub trait StorageReader { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>; + + fn read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> Option<T> { + self.read_from_contract_storage(self.common_key(key)?) + } + + fn unchecked_read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> T { + self.unchecked_read_from_contract_storage( + self.common_key(key) + .unwrap_or_else(|| panic!("no key set for {key:?}")), + ) + } + + fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>; + + fn unchecked_read_from_contract_storage<T: DeserializeOwned>( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::<T>(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } +} + +// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted +// but this makes it a tad easier and it's only testing code so it's fine +pub trait StorageWriter: StorageReader { + fn set_common_value<T: Serialize>( + &mut self, + key: CommonStorageKeys, + value: &T, + ) -> StdResult<()> { + let key = self + .common_key(key) + .ok_or(StdError::not_found("key not found"))? + .to_vec(); + self.set_storage_value(key, value) + } + + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn set_storage_value<T: Serialize>( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_storage(key, &to_json_vec(value)?); + Ok(()) + } +} + +pub trait ArbitraryContractStorageReader { + fn may_read_from_contract_storage( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> Option<Vec<u8>>; + + fn must_read_from_contract_storage( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> StdResult<Vec<u8>> { + let key = key.as_ref(); + self.may_read_from_contract_storage(address, key) + .ok_or(StdError::not_found(format!("no data under {key:?}"))) + } + + fn may_read_value_from_contract_storage<T: DeserializeOwned>( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> StdResult<Option<T>> { + let Some(bytes) = self.may_read_from_contract_storage(address, key) else { + return Ok(None); + }; + + from_json(&bytes).map(Some) + } + + fn must_read_value_from_contract_storage<T: DeserializeOwned>( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> StdResult<T> { + let bytes = self.must_read_from_contract_storage(address, key)?; + from_json(&bytes) + } +} + +pub trait ArbitraryContractStorageWriter { + fn set_contract_storage( + &mut self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ); + + fn set_contract_storage_value<T: Serialize>( + &mut self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_contract_storage(address, key, &to_json_vec(value)?); + Ok(()) + } + + // attempts to write to an arbitrary contract `cw_storage_plus::Map` + fn set_contract_map_value<'a, K, T>( + &mut self, + address: impl Into<String>, + namespace: impl AsRef<[u8]>, + key: K, + value: &T, + ) -> StdResult<()> + where + K: PrimaryKey<'a>, + T: Serialize + DeserializeOwned, + { + let key_path: Path<T> = Path::new( + namespace.as_ref(), + &key.key().iter().map(Key::as_ref).collect::<Vec<_>>(), + ); + let storage_key = key_path.deref(); + self.set_contract_storage_value(address, storage_key, value) + } +} + +// contract that has an admin +pub trait AdminExt: StorageReader + StorageWriter { + fn admin(&self) -> Option<Addr> { + self.read_common_value(CommonStorageKeys::Admin) + } + + fn update_admin(&mut self, admin: &Option<Addr>) -> StdResult<()> { + self.set_common_value(CommonStorageKeys::Admin, admin) + } + + fn admin_unchecked(&self) -> Addr { + self.admin().expect("no admin set") + } + + fn admin_msg(&self) -> MessageInfo { + message_info(&self.admin_unchecked(), &[]) + } +} + +// contract that operates on some specific coin denom +pub trait DenomExt: StorageReader { + fn denom(&self) -> String { + self.unchecked_read_common_value(CommonStorageKeys::Denom) + } + + fn coin(&self, amount: u128) -> Coin { + coin(amount, self.denom()) + } + + fn coins(&self, amount: u128) -> Vec<Coin> { + coins(amount, self.denom()) + } +} + +pub trait RandExt { + fn raw_rng(&mut self) -> &mut ChaCha20Rng; + + fn generate_account(&mut self) -> Addr; + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt; +} + +pub trait BankExt { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>; +} + +impl<T> AdminExt for T where T: StorageReader + StorageWriter {} +impl<T> DenomExt for T where T: StorageReader {} + +impl<C: TestableNymContract> StorageReader for ContractTester<C> { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> { + self.common_storage_keys.get(&key).map(|v| &**v) + } + + fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> { + <Self as ContractOpts>::read_from_contract_storage(self, key) + } +} + +impl<C: TestableNymContract> StorageWriter for ContractTester<C> { + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + <Self as ContractOpts>::set_contract_storage(self, key, value) + } +} + +impl<C: TestableNymContract> BankExt for ContractTester<C> { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> { + self.app + .send_tokens(self.master_address.clone(), to, &[amount])?; + Ok(()) + } +} + +impl<C: TestableNymContract> RandExt for ContractTester<C> { + fn raw_rng(&mut self) -> &mut ChaCha20Rng { + &mut self.rng + } + + fn generate_account(&mut self) -> Addr { + self.app + .api() + .addr_make(&format!("foomp{}", self.rng.next_u64())) + } + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt, + { + let addr = self.generate_account(); + let million = 1_000_000_000_000; + self.send_tokens(addr.clone(), coin(million, TEST_DENOM)) + .unwrap(); + addr + } +} + +impl ArbitraryContractStorageReader for StorageWrapper { + fn may_read_from_contract_storage( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> Option<Vec<u8>> { + self.contract_storage_wrapper(&Addr::unchecked(address)) + .get(key.as_ref()) + } +} + +impl ArbitraryContractStorageWriter for StorageWrapper { + fn set_contract_storage( + &mut self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + // yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine + let mut wrapped_storage = self + .clone() + .contract_storage_wrapper(&Addr::unchecked(address)); + wrapped_storage.set(key.as_ref(), value.as_ref()); + } +} + +impl<C> ArbitraryContractStorageReader for ContractTester<C> +where + C: TestableNymContract, +{ + fn may_read_from_contract_storage( + &self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + ) -> Option<Vec<u8>> { + self.storage + .as_inner_storage() + .may_read_from_contract_storage(address, key) + } +} + +impl<C> ArbitraryContractStorageWriter for ContractTester<C> +where + C: TestableNymContract, +{ + fn set_contract_storage( + &mut self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs new file mode 100644 index 0000000000..2d041a0933 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs @@ -0,0 +1,276 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::{mock_api, test_rng, TEST_DENOM}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper, + Record, Response, Storage, +}; +use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; + +pub use basic_traits::*; +pub use extensions::*; + +pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper}; + +mod basic_traits; +mod extensions; +mod storage_wrapper; +// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now) + +pub type ContractFn<T, E> = + fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result<Response, E>; +pub type QueryFn<T, E> = fn(deps: Deps, env: Env, msg: T) -> Result<Binary, E>; +pub type PermissionedFn<T, E> = fn(deps: DepsMut, env: Env, msg: T) -> Result<Response, E>; + +pub type ContractClosure<T, E> = Box<dyn Fn(DepsMut, Env, MessageInfo, T) -> Result<Response, E>>; +pub type QueryClosure<T, E> = Box<dyn Fn(Deps, Env, T) -> Result<Binary, E>>; + +pub trait TestableNymContract { + const NAME: &'static str; + + type InitMsg: DeserializeOwned + Serialize + Debug + 'static; + type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static; + type QueryMsg: DeserializeOwned + Serialize + Debug + 'static; + type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static; + type ContractError: Display + Debug + Send + Sync + 'static; + + fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError>; + fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError>; + fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>; + fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>; + + fn base_init_msg() -> Self::InitMsg; + + // // for now we don't care about custom queriers + // fn contract_wrapper() -> ContractWrapper< + // Self::ExecuteMsg, + // Self::InitMsg, + // Self::QueryMsg, + // Self::ContractError, + // anyhow::Error, + // anyhow::Error, + // Empty, + // Empty, + // Empty, + // Self::ContractError, + // Self::ContractError, + // Self::MigrateMsg, + // Self::ContractError, + // > { + // ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + // .with_migrate(Self::migrate()) + // } + + fn dyn_contract() -> Box<dyn Contract<Empty>> { + Box::new( + ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + .with_migrate(Self::migrate()), + ) + } + + fn init() -> ContractTester<Self> + where + Self: Sized, + { + ContractTesterBuilder::new() + .instantiate::<Self>(None) + .build() + } +} + +pub struct ContractTesterBuilder<C> { + contract: PhantomData<C>, + master_address: Addr, + app: App<BankKeeper, MockApi, StorageWrapper>, + storage: StorageWrapper, + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl<C> ContractTesterBuilder<C> { + #[allow(clippy::new_without_default)] + pub fn new() -> Self + where + C: TestableNymContract, + { + let storage = StorageWrapper::new(); + + let api = mock_api(); + let master_address = api.addr_make("master-owner"); + + let app = AppBuilder::new() + .with_api(api) + .with_storage(storage.clone()) + .build(|router, _api, storage| { + router + .bank + .init_balance( + storage, + &master_address, + coins(1000000000000000, TEST_DENOM), + ) + .unwrap() + }); + + ContractTesterBuilder { + contract: Default::default(), + master_address, + app, + storage, + well_known_contracts: Default::default(), + } + } + + pub fn instantiate<D: TestableNymContract>( + mut self, + custom_init_msg: Option<D::InitMsg>, + ) -> ContractTesterBuilder<C> { + let code_id = self.app.store_code(D::dyn_contract()); + let contract_address = self + .app + .instantiate_contract( + code_id, + self.master_address.clone(), + &custom_init_msg.unwrap_or(D::base_init_msg()), + &[], + D::NAME, + Some(self.master_address.to_string()), + ) + .unwrap(); + + // send some tokens to the contract + self.app + .send_tokens( + self.master_address.clone(), + contract_address.clone(), + &[coin(100000000, TEST_DENOM)], + ) + .unwrap(); + + self.well_known_contracts.insert(D::NAME, contract_address); + self + } + + pub fn build(self) -> ContractTester<C> + where + C: TestableNymContract, + { + if !self.well_known_contracts.contains_key(C::NAME) { + panic!("{} contract has not been instantiated", C::NAME); + } + + let contract_address = self.well_known_contracts[C::NAME].clone(); + + ContractTester { + contract: self.contract, + app: self.app, + rng: test_rng(), + master_address: self.master_address, + storage: self.storage.contract_storage_wrapper(&contract_address), + contract_address, + common_storage_keys: Default::default(), + well_known_contracts: self.well_known_contracts, + } + } + + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + self.storage.contract_storage_wrapper(contract) + } + + pub fn api(&self) -> MockApi { + *self.app.api() + } + + pub fn querier(&self) -> QuerierWrapper { + self.app.wrap() + } +} + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +pub enum CommonStorageKeys { + Admin, + Denom, +} + +pub struct ContractTester<C: TestableNymContract> { + contract: PhantomData<C>, + pub app: App<BankKeeper, MockApi, StorageWrapper>, + pub rng: ChaCha20Rng, + pub contract_address: Addr, + pub master_address: Addr, + pub(crate) storage: ContractStorageWrapper, + pub common_storage_keys: HashMap<CommonStorageKeys, Vec<u8>>, + + // TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being) + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl<C> ContractTester<C> +where + C: TestableNymContract, +{ + pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) { + self.common_storage_keys + .insert(key, value.as_ref().to_vec()); + } + + pub fn with_common_storage_key( + mut self, + key: CommonStorageKeys, + value: impl AsRef<[u8]>, + ) -> Self { + self.insert_common_storage_key(key, value); + self + } +} + +impl<C> Storage for ContractTester<C> +where + C: TestableNymContract, +{ + fn get(&self, key: &[u8]) -> Option<Vec<u8>> { + self.storage.get(key) + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box<dyn Iterator<Item = Record> + 'a> { + self.storage.range(start, end, order) + } + + fn range_keys<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> { + self.storage.range_keys(start, end, order) + } + + fn range_values<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> { + self.storage.range_values(start, end, order) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + self.storage.set(key, value) + } + + fn remove(&mut self, key: &[u8]) { + self.storage.remove(key) + } +} diff --git a/contracts/nym-pool/src/testing/storage.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs similarity index 89% rename from contracts/nym-pool/src/testing/storage.rs rename to common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs index 14ba3e955a..ab60628a02 100644 --- a/contracts/nym-pool/src/testing/storage.rs +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs @@ -11,7 +11,7 @@ use std::rc::Rc; pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>); impl StorageWrapper { - pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { ContractStorageWrapper { address: contract.clone(), inner: self.clone(), @@ -24,7 +24,7 @@ impl StorageWrapper { } #[derive(Debug, Clone)] -pub(crate) struct ContractStorageWrapper { +pub struct ContractStorageWrapper { address: Addr, inner: StorageWrapper, } @@ -33,6 +33,22 @@ impl ContractStorageWrapper { pub fn inner_storage(&self) -> StorageWrapper { self.inner.clone() } + + pub fn as_inner_storage(&self) -> &StorageWrapper { + &self.inner + } + + pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper { + &mut self.inner + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn change_contract(&self, contract: &Addr) -> Self { + ContractStorageWrapper { + address: contract.clone(), + inner: self.inner.clone(), + } + } } impl Storage for StorageWrapper { diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 4a80db6afa..c13d29a7ec 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } serde_json = { workspace = true } [build-dependencies] diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e0f5845fa0..e8657fb265 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -35,7 +35,7 @@ pub enum ContractsCommonError { /// Percent represents a value between 0 and 100% /// (i.e. between 0.0 and 1.0) #[cw_serde] -#[derive(Copy, Default, PartialOrd)] +#[derive(Copy, Default, PartialOrd, Ord, Eq)] pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { @@ -80,6 +80,44 @@ impl Percent { pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> { self.0.checked_pow(exp).map(Percent) } + + // truncate provided percent to only have 2 decimal places, + // e.g. convert "0.1234567" into "0.12" + // the purpose of it is to reduce storage space, in particular for the performance contract + // since that extra precision gains us nothing + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn round_to_two_decimal_places(&self) -> Self { + let raw = self.0; + + const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 + const THRESHOLD: Decimal = Decimal::permille(5); // 0.005 + + // in case it ever changes since it's not exposed in the public API + debug_assert_eq!( + DECIMAL_FRACTIONAL, + Uint128::new(10).pow(Decimal::DECIMAL_PLACES) + ); + + let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL; + + #[allow(clippy::unwrap_used)] + let floored = Decimal::from_atomics(int, 2).unwrap(); + let diff = raw - floored; + let rounded = if diff >= THRESHOLD { + // ceil + floored + Decimal::percent(1) + } else { + floored + }; + Percent(rounded) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn average(&self, other: &Self) -> Self { + let sum = self.0 + other.0; + let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2)); + Percent(inner) + } } impl Display for Percent { @@ -334,6 +372,7 @@ mod tests { } #[test] + #[cfg(feature = "naive_float")] fn naive_float_conversion() { // around 15 decimal places is the maximum precision we can handle // which is still way more than enough for what we use it for @@ -347,4 +386,41 @@ mod tests { assert!(converted.0 - converted.0 < epsilon); } + + #[test] + fn rounding_percent() { + let test_cases = vec![ + ("0", "0"), + ("0.1", "0.1"), + ("0.12", "0.12"), + ("0.12", "0.123"), + ("0.12", "0.123456789"), + ("0.13", "0.125"), + ("0.13", "0.126"), + ("0.13", "0.126436545676"), + ("0.99", "0.99"), + ("0.99", "0.994"), + ("1", "0.999"), + ("1", "0.995"), + ]; + for (expected, input) in test_cases { + let expected: Percent = expected.parse().unwrap(); + let pre_truncated: Percent = input.parse().unwrap(); + assert_eq!(expected, pre_truncated.round_to_two_decimal_places()) + } + } + + #[test] + fn calculating_average() -> anyhow::Result<()> { + fn p(raw: &str) -> Percent { + raw.parse().unwrap() + } + + assert_eq!(p("0.1").average(&p("0.1")), p("0.1")); + assert_eq!(p("0.1").average(&p("0.2")), p("0.15")); + assert_eq!(p("1").average(&p("0")), p("0.5")); + assert_eq!(p("0.123").average(&p("0.456")), p("0.2895")); + + Ok(()) + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index abe502c50e..d0a7b3b913 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] } schemars = { workspace = true } thiserror = { workspace = true } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } -serde-json-wasm = { workspace = true } humantime-serde = { workspace = true } utoipa = { workspace = true, optional = true } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index deeb416887..72e848aef1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -3,7 +3,7 @@ use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{to_json_string, Addr, Coin}; use std::cmp::Ordering; use std::fmt::Display; @@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate { impl GatewayConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 64b69fe737..91b4222fbe 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -16,7 +16,7 @@ use crate::{ Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; +use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -604,7 +604,7 @@ pub struct NodeCostParams { impl NodeCostParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } @@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate { impl MixNodeConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 55bba8f9a6..e74a56e122 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal; use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Decimal; +use cosmwasm_std::{to_json_string, Decimal}; pub type Performance = Percent; pub type WorkFactor = Decimal; @@ -84,7 +84,7 @@ pub struct IntervalRewardParams { impl IntervalRewardParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } @@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate { } pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml new file mode 100644 index 0000000000..078351a348 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nym-performance-contract-common" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true } +schemars = { workspace = true } + +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } + +nym-contracts-common = { path = "../contracts-common" } + + +[features] +schema = [] + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs new file mode 100644 index 0000000000..b452f95916 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +pub mod storage_keys { + pub const CONTRACT_ADMIN: &str = "contract-admin"; + pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id"; + pub const MIXNET_CONTRACT: &str = "mixnet-contract"; + pub const AUTHORISED_COUNT: &str = "authorised-count"; + pub const AUTHORISED: &str = "authorised"; + pub const RETIRED: &str = "retired"; + pub const PERFORMANCE_RESULTS: &str = "performance-results"; + pub const SUBMISSION_METADATA: &str = "submission-metadata"; +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs new file mode 100644 index 0000000000..68d4e8832b --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId}; +use cosmwasm_std::Addr; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum NymPerformanceContractError { + #[error("could not perform contract migration: {comment}")] + FailedMigration { comment: String }, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error(transparent)] + StdErr(#[from] cosmwasm_std::StdError), + + #[error("{address} is already an authorised network monitor")] + AlreadyAuthorised { address: Addr }, + + #[error("{address} is not an authorised network monitor")] + NotAuthorised { address: Addr }, + + #[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")] + StalePerformanceSubmission { + epoch_id: EpochId, + node_id: NodeId, + last_epoch_id: EpochId, + last_node_id: NodeId, + }, + + #[error("the batch performance data has not been sorted")] + UnsortedBatchSubmission, + + #[error("node {node_id} does not appear to be bonded")] + NodeNotBonded { node_id: NodeId }, +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs new file mode 100644 index 0000000000..4fc43b8bfe --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod types; + +pub use error::*; +pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs new file mode 100644 index 0000000000..cf03b650b7 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -0,0 +1,121 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId, NodePerformance}; +use cosmwasm_schema::cw_serde; + +#[cfg(feature = "schema")] +use crate::types::{ + EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse, + NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse, + RetiredNetworkMonitorsPagedResponse, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub mixnet_contract_address: String, + pub authorised_network_monitors: Vec<String>, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { admin: String }, + + /// Attempt to submit performance data of a particular node for given epoch + Submit { + epoch: EpochId, + data: NodePerformance, + }, + + /// Attempt to submit performance data of a batch of nodes for given epoch + BatchSubmit { + epoch: EpochId, + data: Vec<NodePerformance>, + }, + + /// Attempt to authorise new network monitor for submitting performance data + AuthoriseNetworkMonitor { address: String }, + + /// Attempt to retire an existing network monitor and forbid it from submitting any future performance data + RetireNetworkMonitor { address: String }, + + /// An admin method to remove submitted node measurements. Used as an escape hatch should + /// the data stored get too unwieldy. + RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// An admin method to remove submitted nodes measurements. Used as an escape hatch should + /// the data stored get too unwieldy. Note: it is expected to get called multiple times + /// until the response indicates all the epoch data has been removed. + RemoveEpochMeasurements { epoch_id: EpochId }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] + Admin {}, + + /// Returns performance of particular node for the provided epoch + #[cfg_attr(feature = "schema", returns(NodePerformanceResponse))] + NodePerformance { epoch_id: EpochId, node_id: NodeId }, + + /// Returns historical performance for particular node + #[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))] + NodePerformancePaged { + node_id: NodeId, + start_after: Option<EpochId>, + limit: Option<u32>, + }, + + /// Returns all submitted measurements for the particular node + #[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))] + NodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// Returns (paged) measurements for particular epoch + #[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))] + EpochMeasurementsPaged { + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, + }, + + /// Returns (paged) performance for particular epoch + #[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))] + EpochPerformancePaged { + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, + }, + + /// Returns full (paged) historical performance of the whole network + #[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))] + FullHistoricalPerformancePaged { + start_after: Option<(EpochId, NodeId)>, + limit: Option<u32>, + }, + + /// Returns information about particular network monitor + #[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))] + NetworkMonitor { address: String }, + + /// Returns information about all network monitors + #[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))] + NetworkMonitorsPaged { + start_after: Option<String>, + limit: Option<u32>, + }, + + /// Returns information about all retired network monitors + #[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))] + RetiredNetworkMonitorsPaged { + start_after: Option<String>, + limit: Option<u32>, + }, +} + +#[cw_serde] +pub struct MigrateMsg { + // +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs new file mode 100644 index 0000000000..10ef055d28 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -0,0 +1,242 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Env}; +use nym_contracts_common::Percent; + +pub type EpochId = u32; +pub type NodeId = u32; + +#[cw_serde] +pub struct NetworkMonitorDetails { + pub address: Addr, + pub authorised_by: Addr, + pub authorised_at_height: u64, +} + +impl NetworkMonitorDetails { + pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor { + RetiredNetworkMonitor { + details: self, + retired_by: sender.clone(), + retired_at_height: env.block.height, + } + } +} + +#[cw_serde] +pub struct RetiredNetworkMonitor { + pub details: NetworkMonitorDetails, + pub retired_by: Addr, + pub retired_at_height: u64, +} + +#[cw_serde] +#[derive(Copy)] +pub struct NodePerformance { + #[serde(rename = "n")] + pub node_id: NodeId, + + // note: value is rounded to 2 decimal places. + #[serde(rename = "p")] + pub performance: Percent, +} + +#[cw_serde] +pub struct NetworkMonitorSubmissionMetadata { + pub last_submitted_epoch_id: EpochId, + pub last_submitted_node_id: NodeId, +} + +// the internal values are always sorted +#[cw_serde] +pub struct NodeResults(Vec<Percent>); + +impl NodeResults { + pub fn new(initial: Percent) -> NodeResults { + NodeResults(vec![initial.round_to_two_decimal_places()]) + } + + // ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values + // to insert new one and resave is cheap + pub fn insert_new(&mut self, result: Percent) { + let result = result.round_to_two_decimal_places(); + let pos = self.0.binary_search(&result).unwrap_or_else(|e| e); + self.0.insert(pos, result); + } + + // SAFETY: there are no codepaths that allow constructing empty struct + pub fn median(&self) -> Percent { + let len = self.0.len(); + if len % 2 == 1 { + // odd number of elements: return the middle one + self.0[len / 2] + } else { + // even number: average the two middle elements + let mid1 = self.0[len / 2 - 1]; + let mid2 = self.0[len / 2]; + mid1.average(&mid2).round_to_two_decimal_places() + } + } + + pub fn inner(&self) -> &[Percent] { + &self.0 + } +} + +#[cw_serde] +pub struct NodePerformanceResponse { + pub performance: Option<Percent>, +} + +#[cw_serde] +pub struct NodeMeasurementsResponse { + pub measurements: Option<NodeResults>, +} + +#[cw_serde] +#[derive(Copy)] +pub struct EpochNodePerformance { + pub epoch: EpochId, + pub performance: Option<Percent>, +} + +#[cw_serde] +pub struct NodePerformancePagedResponse { + pub node_id: NodeId, + pub performance: Vec<EpochNodePerformance>, + pub start_next_after: Option<EpochId>, +} + +#[cw_serde] +pub struct EpochPerformancePagedResponse { + pub epoch_id: EpochId, + pub performance: Vec<NodePerformance>, + pub start_next_after: Option<NodeId>, +} + +#[cw_serde] +pub struct NodeMeasurement { + pub node_id: NodeId, + pub measurements: NodeResults, +} + +#[cw_serde] +pub struct EpochMeasurementsPagedResponse { + pub epoch_id: EpochId, + pub measurements: Vec<NodeMeasurement>, + pub start_next_after: Option<NodeId>, +} + +#[cw_serde] +#[derive(Copy)] +pub struct HistoricalPerformance { + pub epoch_id: EpochId, + pub node_id: NodeId, + pub performance: Percent, +} + +#[cw_serde] +pub struct FullHistoricalPerformancePagedResponse { + pub performance: Vec<HistoricalPerformance>, + pub start_next_after: Option<(EpochId, NodeId)>, +} + +#[cw_serde] +pub struct NetworkMonitorInformation { + pub details: NetworkMonitorDetails, + pub current_submission_metadata: NetworkMonitorSubmissionMetadata, +} + +#[cw_serde] +pub struct NetworkMonitorResponse { + pub info: Option<NetworkMonitorInformation>, +} + +#[cw_serde] +pub struct NetworkMonitorsPagedResponse { + pub info: Vec<NetworkMonitorInformation>, + pub start_next_after: Option<String>, +} + +#[cw_serde] +pub struct RetiredNetworkMonitorsPagedResponse { + pub info: Vec<RetiredNetworkMonitor>, + pub start_next_after: Option<String>, +} + +#[cw_serde] +pub struct RemoveEpochMeasurementsResponse { + pub additional_entries_to_remove_remaining: bool, +} + +#[cw_serde] +#[derive(Default)] +pub struct BatchSubmissionResult { + pub accepted_scores: u64, + pub non_existent_nodes: Vec<NodeId>, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn p(raw: impl AsRef<str>) -> Percent { + raw.as_ref().parse().unwrap() + } + + fn ps(raw: &[&str]) -> Vec<Percent> { + raw.iter().map(p).collect() + } + + #[test] + fn node_results_insertion() { + let initial = NodeResults::new(p("0.5")); + + let mut smaller = initial.clone(); + let mut greater = initial.clone(); + + smaller.insert_new(p("0.4")); + greater.insert_new(p("0.6")); + + assert_eq!(smaller.0, ps(&["0.4", "0.5"])); + assert_eq!(greater.0, ps(&["0.5", "0.6"])); + + let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"])); + another.insert_new(p("0.6")); + another.insert_new(p("0.2")); + another.insert_new(p("0.7")); + another.insert_new(p("0.3")); + another.insert_new(p("0.3")); + another.insert_new(p("0.55")); + + assert_eq!( + another.0, + ps(&[ + "0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0" + ]) + ); + } + + #[test] + fn node_results_median() { + let results = NodeResults(ps(&["0.1"])); + assert_eq!(results.median(), p("0.1")); + + let results = NodeResults(ps(&["0.1", "0.2"])); + assert_eq!(results.median(), p("0.15")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3"])); + assert_eq!(results.median(), p("0.2")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"])); + assert_eq!(results.median(), p("0.25")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"])); + assert_eq!(results.median(), p("0.3")); + + let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"])); + assert_eq!(results.median(), p("1")); + } +} diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index c576425398..ab597906c2 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; + +// \/ TODO: this has to be updated once the contract is deployed +pub const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + pub const ECASH_CONTRACT_ADDRESS: &str = "n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun"; pub const GROUP_CONTRACT_ADDRESS: &str = diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 3cd7f7740c..28cf7e2349 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -20,6 +20,8 @@ pub struct ChainDetails { pub struct NymContracts { pub mixnet_contract_address: Option<String>, pub vesting_contract_address: Option<String>, + #[serde(default)] + pub performance_contract_address: Option<String>, pub ecash_contract_address: Option<String>, pub group_contract_address: Option<String>, pub multisig_contract_address: Option<String>, @@ -175,6 +177,9 @@ impl NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str( + mainnet::PERFORMANCE_CONTRACT_ADDRESS, + ), ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 404d5b287e..a434eb8778 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "ark-bls12-381" @@ -1133,6 +1133,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand", + "rand_chacha", + "serde", +] + [[package]] name = "nym-crypto" version = "0.4.0" @@ -1214,7 +1227,9 @@ dependencies = [ "cw2", "easy-addr", "nym-contracts-common", + "nym-contracts-common-testing", "nym-crypto", + "nym-mixnet-contract", "nym-mixnet-contract-common", "nym-vesting-contract-common", "rand", @@ -1238,7 +1253,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -1276,6 +1290,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-performance-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-crypto", + "nym-mixnet-contract", + "nym-mixnet-contract-common", + "nym-performance-contract-common", + "serde", +] + +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-pool-contract" version = "0.1.0" @@ -1284,14 +1330,11 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", - "cw-multi-test", "cw-storage-plus", "cw2", "nym-contracts-common", + "nym-contracts-common-testing", "nym-pool-contract-common", - "rand", - "rand_chacha", - "serde", ] [[package]] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index c8cee21dfa..ec3239772c 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -9,6 +9,7 @@ members = [ "multisig/cw3-flex-multisig", "multisig/cw4-group", "vesting", + "performance", ] [workspace.package] @@ -64,4 +65,4 @@ dbg_macro = "deny" exit = "deny" panic = "deny" unimplemented = "deny" -unreachable = "deny" \ No newline at end of file +unreachable = "deny" diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 9fa22fe0db..ae2ad99d36 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -26,13 +26,13 @@ name = "mixnet_contract" crate-type = ["cdylib", "rlib"] [dependencies] -mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" } -vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" } -nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" } +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true } cosmwasm-schema = { workspace = true, optional = true } cosmwasm-std = { workspace = true } - cw-controllers = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } @@ -41,16 +41,22 @@ bs58 = { workspace = true } serde = { workspace = true, default-features = false, features = ["derive"] } semver = { workspace = true } + [dev-dependencies] anyhow.workspace = true -rand_chacha = "0.3" -rand = "0.8.5" +rand_chacha = { workspace = true } +rand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" } +# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick) +nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } + [features] default = [] contract-testing = ["mixnet-contract-common/contract-testing"] +testable-mixnet-contract = ["nym-contracts-common-testing"] schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"] [lints] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 522017c762..0703e6dc23 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -649,7 +649,7 @@ pub fn migrate( mod tests { use super::*; use crate::rewards::storage as rewards_storage; - use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::reward_params::{ IntervalRewardParams, RewardedSetParams, RewardingParams, @@ -657,6 +657,7 @@ mod tests { use mixnet_contract_common::{ InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange, }; + use nym_contracts_common_testing::mock_dependencies; use std::time::Duration; #[test] diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 8e1450421d..559f6e7ee9 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -22,3 +22,6 @@ mod support; #[cfg(feature = "contract-testing")] mod testing; mod vesting_migration; + +#[cfg(feature = "testable-mixnet-contract")] +pub mod testable_mixnet_contract; diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 0175e4a279..8d1202df71 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -130,20 +130,22 @@ pub mod tests { use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{message_info, MockApi}; + use cosmwasm_std::testing::message_info; use cosmwasm_std::{Coin, Uint128}; use cw_controllers::AdminError::NotAdmin; use mixnet_contract_common::OperatorsParamsUpdate; + use nym_contracts_common_testing::mock_api; #[test] fn update_contract_rewarding_validator_address() { let mut deps = test_helpers::init_contract(); + let mock_api = mock_api(); let info = message_info(&deps.api.addr_make("not-the-creator"), &[]); let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("not-the-creator").to_string(), + mock_api.addr_make("not-the-creator").to_string(), ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); @@ -151,14 +153,14 @@ pub mod tests { let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("new-good-address").to_string(), + mock_api.addr_make("new-good-address").to_string(), ); assert_eq!( res, Ok( Response::default().add_event(new_rewarding_validator_address_update_event( - MockApi::default().addr_make("rewarder"), - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("rewarder"), + mock_api.addr_make("new-good-address") )) ) ); @@ -166,7 +168,7 @@ pub mod tests { let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap(); assert_eq!( state.rewarding_validator_address, - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("new-good-address") ); assert_eq!( diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 1418f0d4cf..29aafaa92e 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -51,11 +51,11 @@ pub mod test_helpers { use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, }; use crate::support::tests::{legacy, test_helpers}; + use crate::testable_mixnet_contract::MixnetContract; use cosmwasm_std::testing::message_info; - use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; @@ -74,22 +74,24 @@ pub mod test_helpers { use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; use mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor, + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, }; use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, - Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg, - Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, - NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, - OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment, - SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, + Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond, + MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, + NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange, + RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, }; + use nym_contracts_common_testing::TestableNymContract; + use nym_contracts_common_testing::{mock_api, mock_dependencies}; use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519::KeyPair; use rand::distributions::WeightedIndex; @@ -100,13 +102,12 @@ pub mod test_helpers { use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; - use std::time::Duration; pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> { let mut rng = test_rng(); let mut addrs = Vec::with_capacity(n); for i in 0..n { - addrs.push(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64()))); + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); } addrs.sort(); addrs @@ -1820,46 +1821,9 @@ pub mod test_helpers { SignableGatewayBondingMsg::new(nonce, content) } - fn intial_rewarded_set_params() -> RewardedSetParams { - RewardedSetParams { - entry_gateways: 50, - exit_gateways: 70, - mixnodes: 120, - standby: 50, - } - } - - fn initial_rewarding_params() -> InitialRewardingParams { - let reward_pool = 250_000_000_000_000u128; - let staking_supply = 100_000_000_000_000u128; - - InitialRewardingParams { - initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) - initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M - staking_supply_scale_factor: Percent::hundred(), - sybil_resistance: Percent::from_percentage_value(30).unwrap(), - active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), - interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_params: intial_rewarded_set_params(), - } - } - pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> { let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), - vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), - rewarding_denom: TEST_COIN_DENOM.to_string(), - epochs_in_interval: 720, - epoch_duration: Duration::from_secs(60 * 60), - initial_rewarding_params: initial_rewarding_params(), - current_nym_node_version: "1.1.10".to_string(), - version_score_weights: Default::default(), - version_score_params: Default::default(), - profit_margin: Default::default(), - interval_operating_cost: Default::default(), - key_validity_in_epochs: None, - }; + let msg = MixnetContract::base_init_msg(); let env = mock_env(); let info = sender("creator"); instantiate(deps.as_mut(), env, info, msg).unwrap(); diff --git a/contracts/mixnet/src/testable_mixnet_contract.rs b/contracts/mixnet/src/testable_mixnet_contract.rs new file mode 100644 index 0000000000..498e121222 --- /dev/null +++ b/contracts/mixnet/src/testable_mixnet_contract.rs @@ -0,0 +1,89 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +// fine in test code +#![allow(clippy::unwrap_used)] + +use crate::contract::{execute, instantiate, migrate, query}; +use cosmwasm_std::Decimal; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::reward_params::RewardedSetParams; +use mixnet_contract_common::{ + ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg, +}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM, +}; +use std::time::Duration; + +pub use nym_contracts_common_testing::TestableNymContract; + +pub struct MixnetContract; + +fn initial_rewarded_set_params() -> RewardedSetParams { + RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 50, + } +} + +fn initial_rewarding_params() -> InitialRewardingParams { + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + + InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_params: initial_rewarded_set_params(), + } +} + +impl TestableNymContract for MixnetContract { + const NAME: &'static str = "mixnet-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = MixnetContractError; + + fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> { + instantiate + } + + fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> { + execute + } + + fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> { + query + } + + fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + let deps = mock_dependencies(); + InstantiateMsg { + rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), + vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), + rewarding_denom: TEST_DENOM.to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: initial_rewarding_params(), + current_nym_node_version: "1.1.10".to_string(), + version_score_weights: Default::default(), + version_score_params: Default::default(), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), + key_validity_in_epochs: None, + } + } +} diff --git a/contracts/nym-pool/Cargo.toml b/contracts/nym-pool/Cargo.toml index 4011deaa30..5bd54bb3bc 100644 --- a/contracts/nym-pool/Cargo.toml +++ b/contracts/nym-pool/Cargo.toml @@ -25,13 +25,9 @@ cosmwasm-schema = { workspace = true, optional = true } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" } - [dev-dependencies] anyhow = { workspace = true } -serde = { workspace = true } -rand_chacha = { workspace = true } -rand = { workspace = true } -cw-multi-test = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } [features] schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"] diff --git a/contracts/nym-pool/src/contract.rs b/contracts/nym-pool/src/contract.rs index 9978d39fe6..476aaad5f7 100644 --- a/contracts/nym-pool/src/contract.rs +++ b/contracts/nym-pool/src/contract.rs @@ -193,8 +193,8 @@ mod tests { #[cfg(test)] mod setting_initial_grants { use super::*; - use crate::testing::deps_with_balance; use cosmwasm_std::{coin, Order, Storage}; + use nym_contracts_common_testing::deps_with_balance; use nym_pool_contract_common::{Allowance, BasicAllowance, Grant, GranteeAddress}; use std::collections::HashMap; diff --git a/contracts/nym-pool/src/helpers.rs b/contracts/nym-pool/src/helpers.rs index e71b9f1148..129c9089e6 100644 --- a/contracts/nym-pool/src/helpers.rs +++ b/contracts/nym-pool/src/helpers.rs @@ -26,12 +26,13 @@ pub fn validate_usage_coin(storage: &dyn Storage, coin: &Coin) -> Result<(), Nym mod tests { use super::*; use crate::storage::NymPoolStorage; - use crate::testing::TestSetup; + use crate::testing::init_contract_tester; use cosmwasm_std::coin; + use nym_contracts_common_testing::ContractOpts; #[test] fn validating_coin_usage() -> anyhow::Result<()> { - let test = TestSetup::init(); + let test = init_contract_tester(); let storage = NymPoolStorage::new(); let denom = storage.pool_denomination.load(test.storage())?; diff --git a/contracts/nym-pool/src/queries.rs b/contracts/nym-pool/src/queries.rs index 997d2c5d67..2d8dcc57a1 100644 --- a/contracts/nym-pool/src/queries.rs +++ b/contracts/nym-pool/src/queries.rs @@ -182,20 +182,22 @@ pub fn query_granters_paged( mod tests { use super::*; use crate::contract::instantiate; - use crate::testing::{TestSetup, TEST_DENOM}; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; use cosmwasm_std::testing::{message_info, mock_dependencies_with_balance, mock_env}; use cosmwasm_std::{coin, Uint128}; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::{Allowance, BasicAllowance, GranterInformation, InstantiateMsg}; #[cfg(test)] mod admin_query { use super::*; - use crate::testing::TestSetup; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; use nym_pool_contract_common::ExecuteMsg; #[test] fn returns_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let initial_admin = test.admin_unchecked(); @@ -255,7 +257,7 @@ mod tests { #[test] fn total_locked_tokens_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = query_total_locked_tokens(test.deps()).unwrap().locked; assert!(locked.amount.is_zero()); @@ -271,7 +273,7 @@ mod tests { #[test] fn locked_tokens_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee1 = test.add_dummy_grant().grantee; test.lock_allowance(grantee1.as_str(), Uint128::new(1234)); @@ -295,8 +297,13 @@ mod tests { #[cfg(test)] mod locked_tokens_paged_query { use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; - fn lock_sorted(test: &mut TestSetup, count: usize) -> Vec<LockedTokens> { + fn lock_sorted( + test: &mut ContractTester<NymPoolContract>, + count: usize, + ) -> Vec<LockedTokens> { let mut grantees = Vec::new(); for _ in 0..count { @@ -314,7 +321,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); let limit = 42; @@ -324,7 +331,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -337,7 +344,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -352,7 +359,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = lock_sorted(&mut test, 1000); // first page should return 2 results... @@ -371,7 +378,7 @@ mod tests { #[test] fn grant_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); // bad address @@ -433,7 +440,7 @@ mod tests { #[test] fn granter_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let admin = test.admin_unchecked(); let env = test.env(); @@ -482,8 +489,13 @@ mod tests { #[cfg(test)] mod granters_paged_query { use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; - fn granters_sorted(test: &mut TestSetup, count: usize) -> Vec<GranterDetails> { + fn granters_sorted( + test: &mut ContractTester<NymPoolContract>, + count: usize, + ) -> Vec<GranterDetails> { let mut granters = Vec::new(); for _ in 0..count { @@ -504,7 +516,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); let limit = 42; @@ -514,7 +526,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -527,7 +539,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -542,7 +554,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = granters_sorted(&mut test, 1000); // first page should return 2 results... @@ -562,8 +574,13 @@ mod tests { #[cfg(test)] mod grants_paged_query { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractOpts, ContractTester}; - fn grants_sorted(test: &mut TestSetup, count: usize) -> Vec<GrantInformation> { + fn grants_sorted( + test: &mut ContractTester<NymPoolContract>, + count: usize, + ) -> Vec<GrantInformation> { let mut grantees = Vec::new(); for _ in 0..count { @@ -580,7 +597,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); let limit = 42; @@ -590,7 +607,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -603,7 +620,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -619,7 +636,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grants = grants_sorted(&mut test, 1000); // first page should return 2 results... diff --git a/contracts/nym-pool/src/storage.rs b/contracts/nym-pool/src/storage.rs index 03d55ef941..9d01d5796f 100644 --- a/contracts/nym-pool/src/storage.rs +++ b/contracts/nym-pool/src/storage.rs @@ -489,19 +489,21 @@ mod tests { #[cfg(test)] mod nympool_storage { use super::*; - use crate::testing::{TestSetup, TEST_DENOM}; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; use cosmwasm_std::testing::{ mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage, }; use cosmwasm_std::{coin, coins, Empty, OwnedDeps}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[cfg(test)] mod initialisation { use super::*; - use crate::testing::{deps_with_balance, TEST_DENOM}; + use crate::testing::TEST_DENOM; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::{coin, Order}; + use nym_contracts_common_testing::deps_with_balance; use nym_pool_contract_common::BasicAllowance; fn all_grants(storage: &dyn Storage) -> HashMap<GranteeAddress, Grant> { @@ -914,7 +916,7 @@ mod tests { #[test] fn loading_granter_information() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); @@ -941,7 +943,7 @@ mod tests { #[test] fn checking_granter_permission() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); test.add_granter(&granter); @@ -957,7 +959,7 @@ mod tests { #[test] fn ensuring_granter_permission() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); test.add_granter(&granter); @@ -1047,7 +1049,7 @@ mod tests { #[test] fn attempting_to_load_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // doesn't exist... @@ -1070,7 +1072,7 @@ mod tests { #[test] fn loading_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // doesn't exist... @@ -1094,11 +1096,12 @@ mod tests { #[cfg(test)] mod adding_new_granter { use super::*; + use crate::testing::init_contract_tester; use cw_controllers::AdminError; #[test] fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1121,7 +1124,7 @@ mod tests { #[test] fn can_only_be_performed_if_account_is_not_already_a_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1143,7 +1146,7 @@ mod tests { #[test] fn saves_basic_metadata() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1194,10 +1197,11 @@ mod tests { #[cfg(test)] mod removing_granter { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_granter_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1217,7 +1221,7 @@ mod tests { #[test] fn can_only_be_performed_by_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let random_address = test.generate_account(); @@ -1259,7 +1263,7 @@ mod tests { #[test] fn removes_it_from_granter_list() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1284,11 +1288,12 @@ mod tests { #[cfg(test)] mod adding_new_grant { use super::*; + use crate::testing::init_contract_tester; use nym_pool_contract_common::ClassicPeriodicAllowance; #[test] fn can_only_be_done_by_whitelisted_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let not_valid_granter = test.generate_account(); @@ -1319,7 +1324,7 @@ mod tests { #[test] fn cant_be_done_if_grant_already_existed() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1340,7 +1345,7 @@ mod tests { #[test] fn only_accepts_valid_allowances() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // allowance with 0 limit and wrong denom @@ -1364,7 +1369,7 @@ mod tests { #[test] fn explicit_limit_cant_be_larger_than_available_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1401,7 +1406,7 @@ mod tests { assert!(res.is_ok()); // and below the available - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let mut limit = available.clone(); limit.amount -= Uint128::new(1); let allowance = Allowance::Basic(BasicAllowance { @@ -1418,7 +1423,7 @@ mod tests { #[test] fn updates_allowances_initial_state_and_saves_it_to_storage() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1456,10 +1461,11 @@ mod tests { #[cfg(test)] mod spending_part_of_grant { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -1485,7 +1491,7 @@ mod tests { #[test] fn requires_grant_to_be_spendable() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1514,7 +1520,7 @@ mod tests { #[test] fn updates_stored_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1548,7 +1554,7 @@ mod tests { #[test] fn removes_grant_from_storage_if_its_used_up() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1612,7 +1618,7 @@ mod tests { #[test] fn removing_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -1656,10 +1662,11 @@ mod tests { #[cfg(test)] mod revoking_grant { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1684,7 +1691,7 @@ mod tests { #[test] fn can_always_be_called_by_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -1717,7 +1724,7 @@ mod tests { #[test] fn can_be_called_by_original_granter_if_its_still_whitelisted() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1761,7 +1768,7 @@ mod tests { #[test] fn removes_the_underlying_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1780,10 +1787,12 @@ mod tests { #[cfg(test)] mod locking_part_of_allowance { use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::DenomExt; #[test] fn requires_providing_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -1804,7 +1813,7 @@ mod tests { #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); let env = test.env(); @@ -1826,7 +1835,7 @@ mod tests { #[test] fn does_not_allow_locking_more_than_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1853,7 +1862,7 @@ mod tests { #[test] fn deducts_locked_amount_from_the_allowance() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1892,7 +1901,7 @@ mod tests { #[test] fn preserves_grant_even_if_resultant_allowance_is_zero() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1918,7 +1927,7 @@ mod tests { #[test] fn updates_internal_locked_counter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let env = test.env(); let grantee = test.add_dummy_grant().grantee; @@ -1953,8 +1962,10 @@ mod tests { #[cfg(test)] mod unlocking_part_of_allowance { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractTester, DenomExt}; - fn setup_locked_grant(test: &mut TestSetup) -> Addr { + fn setup_locked_grant(test: &mut ContractTester<NymPoolContract>) -> Addr { let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); grantee @@ -1962,7 +1973,7 @@ mod tests { #[test] fn requires_providing_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = setup_locked_grant(&mut test); @@ -1981,7 +1992,7 @@ mod tests { #[test] fn does_not_allow_unlocking_more_than_currently_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = setup_locked_grant(&mut test); @@ -1999,7 +2010,7 @@ mod tests { #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -2018,7 +2029,7 @@ mod tests { #[test] fn requires_having_locked_coins() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -2036,7 +2047,7 @@ mod tests { #[test] fn increases_internal_grant_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -2082,7 +2093,7 @@ mod tests { #[test] fn updates_internal_locked_counter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // 100tokens locked @@ -2116,8 +2127,9 @@ mod tests { #[cfg(test)] mod locked_storage { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; use cosmwasm_std::testing::mock_dependencies; + use nym_contracts_common_testing::{ContractOpts, RandExt}; #[test] fn is_initialised_with_zero_total_locked() -> anyhow::Result<()> { @@ -2136,7 +2148,7 @@ mod tests { #[test] fn getting_grantee_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.generate_account(); let storage = LockedStorage::new(); @@ -2167,7 +2179,7 @@ mod tests { #[test] fn getting_maybe_grantee_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.generate_account(); let storage = LockedStorage::new(); @@ -2198,7 +2210,7 @@ mod tests { #[test] fn locking_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = LockedStorage::new(); let grantee1 = test.generate_account(); @@ -2259,7 +2271,7 @@ mod tests { #[test] fn unlocking_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = LockedStorage::new(); let grantee1 = test.generate_account(); diff --git a/contracts/nym-pool/src/testing/mod.rs b/contracts/nym-pool/src/testing/mod.rs index 701dd953de..640b8eccd0 100644 --- a/contracts/nym-pool/src/testing/mod.rs +++ b/contracts/nym-pool/src/testing/mod.rs @@ -1,226 +1,70 @@ // Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::contract; use crate::contract::{execute, instantiate, migrate, query}; use crate::storage::NYM_POOL_STORAGE; -use crate::testing::storage::{ContractStorageWrapper, StorageWrapper}; -use cosmwasm_std::testing::{message_info, mock_env, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{ - coin, coins, Addr, Coin, ContractInfo, Deps, DepsMut, Empty, Env, MemoryStorage, MessageInfo, - Order, OwnedDeps, Response, StdResult, Storage, Uint128, -}; -use cw_multi_test::{ - next_block, App, AppBuilder, AppResponse, BankKeeper, Contract, ContractWrapper, Executor, +use cosmwasm_std::{Addr, Order, Uint128}; +use nym_contracts_common_testing::{ + AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt, + PermissionedFn, QueryFn, RandExt, TestableNymContract, }; +use nym_pool_contract_common::constants::storage_keys; use nym_pool_contract_common::{ - Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, NymPoolContractError, QueryMsg, + Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, MigrateMsg, NymPoolContractError, + QueryMsg, }; -use rand::{RngCore, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use serde::de::DeserializeOwned; use std::collections::HashMap; -mod storage; +pub use nym_contracts_common_testing::TEST_DENOM; -pub fn test_rng() -> ChaCha20Rng { - let dummy_seed = [42u8; 32]; - ChaCha20Rng::from_seed(dummy_seed) -} +pub struct NymPoolContract; -pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::<Empty>::new(&[( - env.contract.address.as_str(), - coins(100000000000, TEST_DENOM).as_slice(), - )]), - custom_query_type: Default::default(), +impl TestableNymContract for NymPoolContract { + const NAME: &'static str = "nym-pool-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPoolContractError; + + fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> { + instantiate + } + + fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> { + execute + } + + fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> { + query + } + + fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + } } } -pub const TEST_DENOM: &str = "unym"; - -pub struct TestSetup { - pub app: App<BankKeeper, MockApi, StorageWrapper>, - pub rng: ChaCha20Rng, - pub contract_address: Addr, - pub master_address: Addr, - pub(crate) storage: ContractStorageWrapper, +pub fn init_contract_tester() -> ContractTester<NymPoolContract> { + NymPoolContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) + .with_common_storage_key(CommonStorageKeys::Denom, storage_keys::POOL_DENOMINATION) } -pub fn contract() -> Box<dyn Contract<Empty>> { - let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); - Box::new(contract) -} - -impl TestSetup { - pub fn init() -> TestSetup { - let storage = StorageWrapper::new(); - - let api = MockApi::default().with_prefix("n"); - let master_address = api.addr_make("master-owner"); - - let mut app = AppBuilder::new() - .with_api(api) - .with_storage(storage.clone()) - .build(|router, _api, storage| { - router - .bank - .init_balance( - storage, - &master_address, - coins(1000000000000000, TEST_DENOM), - ) - .unwrap() - }); - let code_id = app.store_code(contract()); - let contract_address = app - .instantiate_contract( - code_id, - master_address.clone(), - &InstantiateMsg { - pool_denomination: TEST_DENOM.to_string(), - grants: Default::default(), - }, - &[], - "nym-pool-contract", - Some(master_address.to_string()), - ) - .unwrap(); - - // send some tokens to the contract - app.send_tokens( - master_address.clone(), - contract_address.clone(), - &[coin(100000000, TEST_DENOM)], - ) - .unwrap(); - - TestSetup { - app, - rng: test_rng(), - storage: storage.contract_storage_wrapper(&contract_address), - contract_address, - master_address, - } - } - - pub fn set_contract_balance(&mut self, balance: Coin) { - let contract_address = &self.contract_address; - self.app - .router() - .bank - .init_balance( - &mut self.storage.inner_storage(), - contract_address, - vec![balance], - ) - .unwrap(); - } - - pub fn deps(&self) -> Deps<'_> { - Deps { - storage: &self.storage, - api: self.app.api(), - querier: self.app.wrap(), - } - } - - pub fn deps_mut(&mut self) -> DepsMut<'_> { - DepsMut { - storage: &mut self.storage, - api: self.app.api(), - querier: self.app.wrap(), - } - } - - pub fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) { - let env = self.env().clone(); - (self.deps_mut(), env) - } - - pub fn storage(&self) -> &dyn Storage { - &self.storage - } - - pub fn storage_mut(&mut self) -> &mut dyn Storage { - &mut self.storage - } - - pub fn env(&self) -> Env { - Env { - block: self.app.block_info(), - contract: ContractInfo { - address: self.contract_address.clone(), - }, - ..mock_env() - } - } - - pub fn next_block(&mut self) { - self.app.update_block(next_block) - } - - pub fn execute_raw( - &mut self, - sender: Addr, - message: ExecuteMsg, - ) -> Result<Response, NymPoolContractError> { - self.execute_raw_with_balance(sender, &[], message) - } - - pub fn execute_raw_with_balance( - &mut self, - sender: Addr, - coins: &[Coin], - message: ExecuteMsg, - ) -> Result<Response, NymPoolContractError> { - let env = self.env(); - let info = message_info(&sender, coins); - contract::execute(self.deps_mut(), env, info, message) - } - - pub fn execute_msg( - &mut self, - sender: Addr, - message: &ExecuteMsg, - ) -> anyhow::Result<AppResponse> { - self.execute_msg_with_balance(sender, &[], message) - } - - pub fn execute_msg_with_balance( - &mut self, - sender: Addr, - coins: &[Coin], - message: &ExecuteMsg, - ) -> anyhow::Result<AppResponse> { - self.app - .execute_contract(sender, self.contract_address.clone(), message, coins) - } - - pub fn query<T: DeserializeOwned>(&self, message: &QueryMsg) -> StdResult<T> { - self.app - .wrap() - .query_wasm_smart(self.contract_address.as_str(), message) - } - - pub fn generate_account(&mut self) -> Addr { - self.app - .api() - .addr_make(&format!("foomp{}", self.rng.next_u64())) - } - - pub fn admin_unchecked(&self) -> Addr { - NYM_POOL_STORAGE - .contract_admin - .get(self.deps()) - .unwrap() - .unwrap() - } - - pub fn change_admin(&mut self, new_admin: &Addr) { +pub trait NymPoolContractTesterExt: + ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = NymPoolContractError> + + ChainOpts + + AdminExt + + DenomExt + + RandExt +{ + fn change_admin(&mut self, new_admin: &Addr) { self.execute_msg( self.admin_unchecked(), &ExecuteMsg::UpdateAdmin { @@ -231,33 +75,14 @@ impl TestSetup { .unwrap(); } - pub fn admin_msg(&self) -> MessageInfo { - message_info(&self.admin_unchecked(), &[]) - } - - pub fn denom(&self) -> String { - NYM_POOL_STORAGE - .pool_denomination - .load(self.storage()) - .unwrap() - } - - pub fn coin(&self, amount: u128) -> Coin { - coin(amount, self.denom()) - } - - pub fn coins(&self, amount: u128) -> Vec<Coin> { - coins(amount, self.denom()) - } - #[track_caller] - pub fn add_dummy_grant(&mut self) -> Grant { + fn add_dummy_grant(&mut self) -> Grant { let grantee = self.generate_account(); self.add_dummy_grant_for(&grantee) } #[track_caller] - pub fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant { + fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant { let grantee = Addr::unchecked(grantee); let granter = self.admin_unchecked(); let env = self.env(); @@ -275,23 +100,18 @@ impl TestSetup { } #[track_caller] - pub fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) { - let denom = NYM_POOL_STORAGE - .pool_denomination - .load(self.deps().storage) - .unwrap(); - + fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) { self.execute_msg( Addr::unchecked(grantee), &ExecuteMsg::LockAllowance { - amount: coin(amount.into().u128(), denom), + amount: self.coin(amount.into().u128()), }, ) .unwrap(); } #[track_caller] - pub fn full_locked_map(&self) -> HashMap<Addr, Uint128> { + fn full_locked_map(&self) -> HashMap<Addr, Uint128> { NYM_POOL_STORAGE .locked .grantees @@ -301,7 +121,7 @@ impl TestSetup { } #[track_caller] - pub fn add_granter(&mut self, granter: &Addr) { + fn add_granter(&mut self, granter: &Addr) { let env = self.env(); let admin = self.admin_unchecked(); NYM_POOL_STORAGE @@ -309,3 +129,5 @@ impl TestSetup { .unwrap(); } } + +impl NymPoolContractTesterExt for ContractTester<NymPoolContract> {} diff --git a/contracts/nym-pool/src/transactions.rs b/contracts/nym-pool/src/transactions.rs index 59181bd07b..3274a31c34 100644 --- a/contracts/nym-pool/src/transactions.rs +++ b/contracts/nym-pool/src/transactions.rs @@ -262,21 +262,22 @@ pub fn try_remove_expired( #[cfg(test)] mod tests { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::ExecuteMsg; #[cfg(test)] mod updating_contract_admin { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::{Deps, Order}; use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, RandExt}; use nym_pool_contract_common::{ExecuteMsg, GranterAddress, GranterInformation}; use std::collections::HashMap; #[test] fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let random_acc = test.generate_account(); let new_admin = test.generate_account(); @@ -310,7 +311,7 @@ mod tests { #[test] fn requires_providing_valid_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_account = "definitely-not-valid-account"; let res = test.execute_raw( @@ -347,7 +348,7 @@ mod tests { .collect() } - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let current_admin = test.admin_unchecked(); let new_admin = test.generate_account(); @@ -369,7 +370,7 @@ mod tests { // // - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let current_admin = test.admin_unchecked(); let new_admin = test.generate_account(); let old_granters = granters(test.deps()); @@ -392,13 +393,13 @@ mod tests { #[cfg(test)] mod granting_allowance { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let admin = test.admin_msg(); @@ -433,12 +434,12 @@ mod tests { #[cfg(test)] mod revoking_allowance { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; #[test] fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let admin = test.admin_msg(); @@ -487,12 +488,12 @@ mod tests { #[cfg(test)] mod using_allowance { use super::*; - use crate::testing::TestSetup; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; #[test] fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw(grantee, ExecuteMsg::UseAllowance { recipients: vec![] }); @@ -504,7 +505,7 @@ mod tests { #[test] fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { // 1 bad receiver - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -519,7 +520,7 @@ mod tests { assert!(res.is_err()); // 3 receivers, one invalid - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let addr1 = test.generate_account(); @@ -547,7 +548,7 @@ mod tests { assert!(res.is_err()); // all fine - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -576,7 +577,7 @@ mod tests { #[test] fn requires_the_total_to_be_available_for_spending() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let recipient = test.generate_account(); // contract balance < required @@ -665,7 +666,7 @@ mod tests { #[test] fn requires_the_total_to_be_within_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: Some(test.coin(100)), expiration_unix_timestamp: None, @@ -712,7 +713,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; @@ -774,7 +775,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -810,13 +811,14 @@ mod tests { #[cfg(test)] mod withdrawing_from_allowance { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; #[test] fn requires_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -848,7 +850,7 @@ mod tests { #[test] fn requires_the_amount_to_be_available_for_spending() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); // contract balance < required let grantee = test.add_dummy_grant().grantee; @@ -912,7 +914,7 @@ mod tests { #[test] fn requires_the_amount_to_be_within_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: Some(test.coin(100)), expiration_unix_timestamp: None, @@ -952,7 +954,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; @@ -978,7 +980,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1011,7 +1013,7 @@ mod tests { #[test] fn locking_allowance() -> anyhow::Result<()> { // internals got tested in storage tests, so this is mostly about checking events (TODO) - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -1035,7 +1037,7 @@ mod tests { #[test] fn unlocking_allowance() -> anyhow::Result<()> { // internals got tested in storage tests, so this is mostly about checking events (TODO) - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1060,11 +1062,12 @@ mod tests { #[cfg(test)] mod using_locked_allowance { use super::*; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -1079,7 +1082,7 @@ mod tests { #[test] fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { // 1 bad receiver - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1095,7 +1098,7 @@ mod tests { assert!(res.is_err()); // 3 receivers, one invalid - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1124,7 +1127,7 @@ mod tests { assert!(res.is_err()); // all fine - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1154,7 +1157,7 @@ mod tests { #[test] fn requires_the_total_to_be_locked_by_grantee() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1193,7 +1196,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1256,7 +1259,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1294,11 +1297,12 @@ mod tests { mod withdrawing_from_locked_allowance { use super::*; use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1331,7 +1335,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1358,7 +1362,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1391,7 +1395,7 @@ mod tests { #[test] fn requires_the_amount_to_be_locked_by_grantee() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1424,7 +1428,7 @@ mod tests { #[test] fn adding_new_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_address = "foomp"; let good_address = test.generate_account(); @@ -1456,7 +1460,7 @@ mod tests { #[test] fn revoking_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_address = "foomp"; let good_address = test.generate_account(); let granter_address = test.generate_account(); @@ -1500,10 +1504,12 @@ mod tests { #[cfg(test)] mod removing_expired { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, ContractTester, RandExt}; use nym_pool_contract_common::{BasicAllowance, GranteeAddress}; - fn setup_with_expired_grant() -> (TestSetup, GranteeAddress) { - let mut test = TestSetup::init(); + fn setup_with_expired_grant() -> (ContractTester<NymPoolContract>, GranteeAddress) { + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1543,7 +1549,7 @@ mod tests { #[test] fn requires_grant_to_actually_exist_and_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let sender = test.generate_account(); let grantee = test.add_dummy_grant().grantee; let not_grantee = test.generate_account(); diff --git a/contracts/performance/.cargo/config b/contracts/performance/.cargo/config new file mode 100644 index 0000000000..2fb2c1afdb --- /dev/null +++ b/contracts/performance/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/performance/Cargo.toml b/contracts/performance/Cargo.toml new file mode 100644 index 0000000000..abeef17f40 --- /dev/null +++ b/contracts/performance/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-performance-contract" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "schema" +required-features = ["schema-gen"] + +[lib] +name = "nym_performance_contract" +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } +serde = { workspace = true } + +cosmwasm-schema = { workspace = true, optional = true } + +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-performance-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-performance-contract" } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } + +[dev-dependencies] +anyhow = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } +nym-mixnet-contract = { path = "../mixnet", features = ["testable-mixnet-contract"] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } + +[features] +schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"] + +[lints] +workspace = true \ No newline at end of file diff --git a/contracts/performance/Makefile b/contracts/performance/Makefile new file mode 100644 index 0000000000..086fa71ad3 --- /dev/null +++ b/contracts/performance/Makefile @@ -0,0 +1,5 @@ +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +generate-schema: + cargo schema diff --git a/contracts/performance/src/bin/schema.rs b/contracts/performance/src/bin/schema.rs new file mode 100644 index 0000000000..d34f6f9b74 --- /dev/null +++ b/contracts/performance/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_performance_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs new file mode 100644 index 0000000000..235cef391f --- /dev/null +++ b/contracts/performance/src/contract.rs @@ -0,0 +1,187 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::queries::{ + query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, + query_full_historical_performance_paged, query_network_monitor_details, + query_network_monitors_paged, query_node_measurements, query_node_performance, + query_node_performance_paged, query_retired_network_monitors_paged, +}; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use crate::transactions::{ + try_authorise_network_monitor, try_batch_submit_performance_results, + try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor, + try_submit_performance_results, try_update_contract_admin, +}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, +}; +use nym_contracts_common::set_build_information; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NymPerformanceContractError, QueryMsg, +}; + +const CONTRACT_NAME: &str = "crate:nym-performance-contract"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result<Response, NymPerformanceContractError> { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(deps.storage)?; + + let mixnet_contract_address = deps.api.addr_validate(&msg.mixnet_contract_address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.initialise( + deps, + env, + info.sender, + mixnet_contract_address.clone(), + msg.authorised_network_monitors, + )?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result<Response, NymPerformanceContractError> { + match msg { + ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), + ExecuteMsg::Submit { epoch, data } => { + try_submit_performance_results(deps, info, epoch, data) + } + ExecuteMsg::BatchSubmit { epoch, data } => { + try_batch_submit_performance_results(deps, info, epoch, data) + } + ExecuteMsg::AuthoriseNetworkMonitor { address } => { + try_authorise_network_monitor(deps, env, info, address) + } + ExecuteMsg::RetireNetworkMonitor { address } => { + try_retire_network_monitor(deps, env, info, address) + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => { + try_remove_node_measurements(deps, info, epoch_id, node_id) + } + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => { + try_remove_epoch_measurements(deps, info, epoch_id) + } + } +} + +#[entry_point] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NymPerformanceContractError> { + match msg { + QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?), + QueryMsg::NodePerformance { epoch_id, node_id } => Ok(to_json_binary( + &query_node_performance(deps, epoch_id, node_id)?, + )?), + QueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => Ok(to_json_binary(&query_node_performance_paged( + deps, + node_id, + start_after, + limit, + )?)?), + QueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_performance_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + QueryMsg::FullHistoricalPerformancePaged { start_after, limit } => Ok(to_json_binary( + &query_full_historical_performance_paged(deps, start_after, limit)?, + )?), + QueryMsg::NetworkMonitor { address } => Ok(to_json_binary( + &query_network_monitor_details(deps, address)?, + )?), + QueryMsg::NetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_retired_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary( + &query_node_measurements(deps, epoch_id, node_id)?, + )?), + QueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_measurements_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + } +} + +#[entry_point] +pub fn migrate( + deps: DepsMut, + _: Env, + _msg: MigrateMsg, +) -> Result<Response, NymPerformanceContractError> { + set_build_information!(deps.storage)?; + cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod contract_instantiation { + use super::*; + use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; + use crate::testing::PreInitContract; + use cosmwasm_std::testing::message_info; + + #[test] + fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> { + // we need to mock dependencies in a state where mixnet contract has already been instantiated + // (we query it at init) + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let mixnet_contract_address = pre_init.mixnet_contract_address.to_string(); + let some_sender = pre_init.addr_make("some_sender"); + let deps = pre_init.deps_mut(); + + instantiate( + deps, + env, + message_info(&some_sender, &[]), + InstantiateMsg { + mixnet_contract_address, + authorised_network_monitors: vec![], + }, + )?; + + let deps = pre_init.deps(); + + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .assert_admin(deps, &some_sender)?; + + Ok(()) + } + } +} diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs new file mode 100644 index 0000000000..51ccb4f4b2 --- /dev/null +++ b/contracts/performance/src/helpers.rs @@ -0,0 +1,118 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; +use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; +use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond}; +use nym_performance_contract_common::{EpochId, NodeId}; +use serde::de::DeserializeOwned; +use std::ops::Deref; + +pub(crate) trait MixnetContractQuerier { + #[allow(dead_code)] + fn query_mixnet_contract<T: DeserializeOwned>( + &self, + address: impl Into<String>, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult<T>; + + fn query_mixnet_contract_storage( + &self, + address: impl Into<String>, + key: impl Into<Binary>, + ) -> StdResult<Option<Vec<u8>>>; + + fn query_mixnet_contract_storage_value<T: DeserializeOwned>( + &self, + address: impl Into<String>, + key: impl Into<Binary>, + ) -> StdResult<Option<T>> { + match self.query_mixnet_contract_storage(address, key)? { + None => Ok(None), + Some(value) => Ok(Some(from_json(&value)?)), + } + } + + fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> { + self.query_mixnet_contract_storage_value(address, b"ci")? + .ok_or(StdError::not_found( + "unable to retrieve interval information from the mixnet contract storage", + )) + } + + fn query_current_absolute_mixnet_epoch_id( + &self, + address: impl Into<String>, + ) -> StdResult<EpochId> { + self.query_current_mixnet_interval(address) + .map(|interval| interval.current_epoch_absolute_id()) + } + + fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> { + let mixnet_contract_address = address.into(); + + // 1. check if it's a nym-node + if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? { + return Ok(!nym_node.is_unbonding); + } + + // 2. try a legacy mixnode + if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? { + return Ok(!nym_node.is_unbonding); + } + Ok(false) + } + + fn query_nymnode_bond( + &self, + address: impl Into<String>, + node_id: NodeId, + ) -> StdResult<Option<NymNodeBond>> { + // construct proper map key + let pk_namespace = "nn"; + let path: Path<NymNodeBond> = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } + + fn query_mixnode_bond( + &self, + address: impl Into<String>, + node_id: NodeId, + ) -> StdResult<Option<MixNodeBond>> { + // construct proper map key + let pk_namespace = "mnn"; + let path: Path<MixNodeBond> = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } +} + +impl<C> MixnetContractQuerier for QuerierWrapper<'_, C> +where + C: CustomQuery, +{ + fn query_mixnet_contract<T: DeserializeOwned>( + &self, + address: impl Into<String>, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult<T> { + self.query_wasm_smart(address, msg) + } + + fn query_mixnet_contract_storage( + &self, + address: impl Into<String>, + key: impl Into<Binary>, + ) -> StdResult<Option<Vec<u8>>> { + self.query_wasm_raw(address, key) + } +} diff --git a/contracts/performance/src/lib.rs b/contracts/performance/src/lib.rs new file mode 100644 index 0000000000..08a2b9651d --- /dev/null +++ b/contracts/performance/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +pub mod contract; +pub mod queued_migrations; +pub mod storage; + +mod helpers; +mod queries; +mod transactions; + +#[cfg(test)] +pub mod testing; diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs new file mode 100644 index 0000000000..3cc095481a --- /dev/null +++ b/contracts/performance/src/queries.rs @@ -0,0 +1,588 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use cosmwasm_std::{Addr, Deps, Order, StdResult}; +use cw_controllers::AdminResponse; +use cw_storage_plus::Bound; +use nym_performance_contract_common::{ + EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, + NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, + NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, +}; + +pub fn query_admin(deps: Deps) -> Result<AdminResponse, NymPerformanceContractError> { + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .query_admin(deps) + .map_err(Into::into) +} + +pub fn query_node_performance( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result<NodePerformanceResponse, NymPerformanceContractError> { + let performance = + NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(deps.storage, epoch_id, node_id)?; + Ok(NodePerformanceResponse { performance }) +} + +pub fn query_node_measurements( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result<NodeMeasurementsResponse, NymPerformanceContractError> { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .may_load(deps.storage, (epoch_id, node_id))?; + Ok(NodeMeasurementsResponse { measurements }) +} + +pub fn query_node_performance_paged( + deps: Deps, + node_id: NodeId, + start_after: Option<EpochId>, + limit: Option<u32>, +) -> Result<NodePerformancePagedResponse, NymPerformanceContractError> { + let current_epoch_id = NYM_PERFORMANCE_CONTRACT_STORAGE.current_mixnet_epoch_id(deps)?; + + let start = match start_after { + None => NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_epoch_id_at_creation + .load(deps.storage)?, + Some(start_after) => start_after + 1, + }; + + let mut performance = Vec::new(); + + if current_epoch_id < start { + return Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after: None, + }); + } + + let limit = limit + .unwrap_or(retrieval_limits::NODE_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_PERFORMANCE_MAX_LIMIT) as usize; + + for epoch_id in (start..=current_epoch_id).take(limit) { + performance.push(EpochNodePerformance { + epoch: epoch_id, + performance: NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance( + deps.storage, + epoch_id, + node_id, + )?, + }) + } + + let start_next_after = performance.last().and_then(|last| { + if last.epoch != current_epoch_id { + Some(last.epoch) + } else { + None + } + }); + + Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_performance_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, +) -> Result<EpochPerformancePagedResponse, NymPerformanceContractError> { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, results)| NodePerformance { + node_id, + performance: results.median(), + }) + }) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = performance.last().map(|last| last.node_id); + + Ok(EpochPerformancePagedResponse { + epoch_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_measurements_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option<NodeId>, + limit: Option<u32>, +) -> Result<EpochMeasurementsPagedResponse, NymPerformanceContractError> { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, measurements)| NodeMeasurement { + node_id, + measurements, + }) + }) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = measurements.last().map(|last| last.node_id); + + Ok(EpochMeasurementsPagedResponse { + epoch_id, + measurements, + start_next_after, + }) +} + +pub fn query_full_historical_performance_paged( + deps: Deps, + start_after: Option<(EpochId, NodeId)>, + limit: Option<u32>, +) -> Result<FullHistoricalPerformancePagedResponse, NymPerformanceContractError> { + let limit = limit + .unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|((epoch_id, node_id), results)| HistoricalPerformance { + epoch_id, + node_id, + performance: results.median(), + }) + }) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id)); + + Ok(FullHistoricalPerformancePagedResponse { + performance, + start_next_after, + }) +} + +fn get_network_monitor_information( + deps: Deps, + address: &Addr, +) -> Result<Option<NetworkMonitorInformation>, NymPerformanceContractError> { + let Some(details) = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .may_load(deps.storage, address)? + else { + return Ok(None); + }; + + let current_submission_metadata = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, address)?; + + Ok(Some(NetworkMonitorInformation { + details, + current_submission_metadata, + })) +} + +pub fn query_network_monitor_details( + deps: Deps, + address: String, +) -> Result<NetworkMonitorResponse, NymPerformanceContractError> { + let address = deps.api.addr_validate(&address)?; + + Ok(NetworkMonitorResponse { + info: get_network_monitor_information(deps, &address)?, + }) +} + +pub fn query_network_monitors_paged( + deps: Deps, + start_after: Option<String>, + limit: Option<u32>, +) -> Result<NetworkMonitorsPagedResponse, NymPerformanceContractError> { + let limit = limit + .unwrap_or(retrieval_limits::NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.and_then(|(address, details)| { + NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, &address) + .map(|current_submission_metadata| NetworkMonitorInformation { + details, + current_submission_metadata, + }) + }) + }) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(NetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +pub fn query_retired_network_monitors_paged( + deps: Deps, + start_after: Option<String>, + limit: Option<u32>, +) -> Result<RetiredNetworkMonitorsPagedResponse, NymPerformanceContractError> { + let limit = limit + .unwrap_or(retrieval_limits::RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::RETIRED_NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .retired + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| record.map(|(_, details)| details)) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(RetiredNetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{ContractOpts, RandExt}; + + #[cfg(test)] + mod admin_query { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn returns_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let initial_admin = test.admin_unchecked(); + + // initial + let res = query_admin(test.deps())?; + assert_eq!(res.admin, Some(initial_admin.to_string())); + + let new_admin = test.generate_account(); + + // sanity check + assert_ne!(initial_admin, new_admin); + + // after update + test.execute_msg( + initial_admin.clone(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + )?; + + let updated_admin = query_admin(test.deps())?; + assert_eq!(updated_admin.admin, Some(new_admin.to_string())); + + Ok(()) + } + } + + #[test] + fn querying_node_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let node_id = test.bond_dummy_nymnode()?; + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + // epoch 0 + test.insert_raw_performance(&nm, node_id, "0")?; + + // epoch 1 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.1")?; + + // epoch 2 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.2")?; + + // epoch 3 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.3")?; + + // epoch 4 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.4")?; + + // epoch 5 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.5")?; + + let deps = test.deps(); + let res = query_node_performance_paged(deps, node_id, Some(5), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(4), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + }] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, None, None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 0, + performance: Some("0".parse()?), + }, + EpochNodePerformance { + epoch: 1, + performance: Some("0.1".parse()?), + }, + EpochNodePerformance { + epoch: 2, + performance: Some("0.2".parse()?), + }, + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), Some(1))?; + assert_eq!(res.start_next_after, Some(3)); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }] + ); + + Ok(()) + } + + #[test] + fn querying_epoch_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(test.bond_dummy_nymnode()?); + } + + let epoch_id = 5; + test.set_mixnet_epoch(epoch_id)?; + + test.insert_raw_performance(&nm, nodes[1], "0.1")?; + test.insert_raw_performance(&nm, nodes[2], "0.2")?; + test.insert_raw_performance(&nm, nodes[3], "0.3")?; + // 4 is missing + test.insert_raw_performance(&nm, nodes[5], "0.5")?; + test.insert_raw_performance(&nm, nodes[6], "0.6")?; + + let deps = test.deps(); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[4]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[3]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, None, None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[1], + performance: "0.1".parse()?, + }, + NodePerformance { + node_id: nodes[2], + performance: "0.2".parse()?, + }, + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), Some(1))?; + assert_eq!(res.start_next_after, Some(nodes[3])); + assert_eq!( + res.performance, + vec![NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }] + ); + + Ok(()) + } +} diff --git a/contracts/performance/src/queued_migrations.rs b/contracts/performance/src/queued_migrations.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/contracts/performance/src/queued_migrations.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs new file mode 100644 index 0000000000..0559352a28 --- /dev/null +++ b/contracts/performance/src/storage.rs @@ -0,0 +1,2603 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::MixnetContractQuerier; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, Storage}; +use cw_controllers::Admin; +use cw_storage_plus::{Item, Map}; +use nym_contracts_common::Percent; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + BatchSubmissionResult, EpochId, NetworkMonitorDetails, NetworkMonitorSubmissionMetadata, + NodeId, NodePerformance, NodeResults, NymPerformanceContractError, + RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, +}; + +pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = + NymPerformanceContractStorage::new(); + +pub struct NymPerformanceContractStorage { + pub(crate) contract_admin: Admin, + pub(crate) mixnet_epoch_id_at_creation: Item<EpochId>, + + pub(crate) mixnet_contract_address: Item<Addr>, + + pub(crate) network_monitors: NetworkMonitorsStorage, + + pub(crate) performance_results: PerformanceResultsStorage, +} + +impl NymPerformanceContractStorage { + #[allow(clippy::new_without_default)] + pub(crate) const fn new() -> Self { + NymPerformanceContractStorage { + contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), + mixnet_epoch_id_at_creation: Item::new(storage_keys::INITIAL_EPOCH_ID), + mixnet_contract_address: Item::new(storage_keys::MIXNET_CONTRACT), + network_monitors: NetworkMonitorsStorage::new(), + performance_results: PerformanceResultsStorage::new(), + } + } + + pub fn current_mixnet_epoch_id( + &self, + deps: Deps, + ) -> Result<EpochId, NymPerformanceContractError> { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + let current_epoch_id = deps + .querier + .query_current_absolute_mixnet_epoch_id(&mixnet_contract_address)?; + Ok(current_epoch_id) + } + + pub fn node_bonded( + &self, + deps: Deps, + node_id: NodeId, + ) -> Result<bool, NymPerformanceContractError> { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + + let exists = deps + .querier + .check_node_existence(mixnet_contract_address, node_id)?; + Ok(exists) + } + + pub fn initialise( + &self, + mut deps: DepsMut, + env: Env, + admin: Addr, + mixnet_contract_address: Addr, + initial_authorised_network_monitors: Vec<String>, + ) -> Result<(), NymPerformanceContractError> { + // set the mixnet contract address + self.mixnet_contract_address + .save(deps.storage, &mixnet_contract_address)?; + + let initial_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + // set the initial epoch id + self.mixnet_epoch_id_at_creation + .save(deps.storage, &initial_epoch_id)?; + + // set the contract admin + self.contract_admin + .set(deps.branch(), Some(admin.clone()))?; + + // initialise the network monitors storage (by setting the current count to 0) + self.network_monitors.initialise(deps.branch())?; + + // add all initial network monitors + for network_monitor in initial_authorised_network_monitors { + let network_monitor = deps.api.addr_validate(&network_monitor)?; + self.authorise_network_monitor(deps.branch(), &env, &admin, network_monitor)?; + } + + Ok(()) + } + + pub fn submit_performance_data( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + data: NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + // 2. check if current submission metadata is consistent with the result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + // 3. check if the node is bonded + if !self.node_bonded(deps.as_ref(), data.node_id)? { + return Err(NymPerformanceContractError::NodeNotBonded { + node_id: data.node_id, + }); + } + + // 4 insert performance data into the storage + self.performance_results + .insert_performance_data(deps.storage, epoch_id, &data)?; + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + Ok(()) + } + + pub fn batch_submit_performance_results( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + data: Vec<NodePerformance>, + ) -> Result<BatchSubmissionResult, NymPerformanceContractError> { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + let Some(first) = data.first() else { + // no performance data + return Ok(BatchSubmissionResult::default()); + }; + + // 2. check if current submission metadata is consistent with the first result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + first.node_id, + )?; + + let mut accepted_scores = 0; + let mut non_existent_nodes = Vec::new(); + + // 3. submit it + if self.node_bonded(deps.as_ref(), first.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, first)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(first.node_id); + } + + // not point in using peekable iterator, we can just keep track of the previous + // element we've seen + let mut previous = first.node_id; + + for perf in data.iter().skip(1) { + // 4. ensure provided data is sorted (if the check fails in later iteration, + // the whole tx will get reverted so it's fine to just set the storage within the same loop + if perf.node_id <= previous { + return Err(NymPerformanceContractError::UnsortedBatchSubmission); + } + previous = perf.node_id; + + // 5. insert performance data into the storage + if self.node_bonded(deps.as_ref(), perf.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, perf)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(perf.node_id); + } + } + + // SAFETY: we know this vector is not empty + #[allow(clippy::unwrap_used)] + let last = data.last().unwrap(); + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + last.node_id, + )?; + + Ok(BatchSubmissionResult { + accepted_scores, + non_existent_nodes, + }) + } + + #[cfg(test)] + fn is_admin(&self, deps: Deps, addr: &Addr) -> Result<bool, NymPerformanceContractError> { + self.contract_admin.is_admin(deps, addr).map_err(Into::into) + } + + fn ensure_is_admin(&self, deps: Deps, addr: &Addr) -> Result<(), NymPerformanceContractError> { + self.contract_admin + .assert_admin(deps, addr) + .map_err(Into::into) + } + + pub fn authorise_network_monitor( + &self, + mut deps: DepsMut, + env: &Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // make sure this address is not already authorised (it'd mess up the total count) + if self + .network_monitors + .authorised + .has(deps.storage, &network_monitor) + { + return Err(NymPerformanceContractError::AlreadyAuthorised { + address: network_monitor, + }); + } + + // insert the new entry and adjust the total count + self.network_monitors + .insert_new(deps.branch(), env, sender, &network_monitor)?; + + // finally, set submission metadata to disallow this NM from submitting data for epochs before it was authorised + let current_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + self.performance_results.submission_metadata.save( + deps.storage, + &network_monitor, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id: current_epoch_id, + last_submitted_node_id: 0, + }, + )?; + Ok(()) + } + + pub fn retire_network_monitor( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.network_monitors + .retire(deps, &env, sender, &network_monitor) + } + + pub fn try_load_performance( + &self, + storage: &dyn Storage, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<Option<Percent>, NymPerformanceContractError> { + Ok(self + .performance_results + .results + .may_load(storage, (epoch_id, node_id))? + .map(|r| r.median())) + } + + pub fn remove_node_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.performance_results + .results + .remove(deps.storage, (epoch_id, node_id)); + Ok(()) + } + + pub fn remove_epoch_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + ) -> Result<RemoveEpochMeasurementsResponse, NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // 1. purge the entries according to the limit + self.performance_results.results.prefix(epoch_id).clear( + deps.storage, + Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), + ); + + // 2. see if there's anything left + let additional_entries_to_remove_remaining = !self + .performance_results + .results + .prefix(epoch_id) + .is_empty(deps.storage); + + Ok(RemoveEpochMeasurementsResponse { + additional_entries_to_remove_remaining, + }) + } +} + +pub(crate) struct NetworkMonitorsStorage { + pub(crate) authorised_count: Item<u32>, + pub(crate) authorised: Map<&'static Addr, NetworkMonitorDetails>, + pub(crate) retired: Map<&'static Addr, RetiredNetworkMonitor>, +} + +impl NetworkMonitorsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + NetworkMonitorsStorage { + authorised_count: Item::new(storage_keys::AUTHORISED_COUNT), + authorised: Map::new(storage_keys::AUTHORISED), + retired: Map::new(storage_keys::RETIRED), + } + } + + fn initialise(&self, deps: DepsMut) -> Result<(), NymPerformanceContractError> { + self.authorised_count.save(deps.storage, &0)?; + Ok(()) + } + + fn ensure_authorised( + &self, + storage: &dyn Storage, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + if !self.authorised.has(storage, addr) { + return Err(NymPerformanceContractError::NotAuthorised { + address: addr.clone(), + }); + } + Ok(()) + } + + fn insert_new( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + address: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // if this address has already been retired in the past, restore it + self.retired.remove(deps.storage, address); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count + 1) + })?; + self.authorised.save( + deps.storage, + address, + &NetworkMonitorDetails { + address: address.clone(), + authorised_by: sender.clone(), + authorised_at_height: env.block.height, + }, + )?; + Ok(()) + } + + fn retire( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // NOTE: if the NM hasn't been authorised before, the `load` call will fail + // and thus `authorised_count` won't be updated (nor further code executed) + let details = self.authorised.load(deps.storage, addr)?; + self.authorised.remove(deps.storage, addr); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count - 1) + })?; + + self.retired + .save(deps.storage, addr, &details.retire(env, sender))?; + Ok(()) + } +} + +pub(crate) struct PerformanceResultsStorage { + pub(crate) results: Map<(EpochId, NodeId), NodeResults>, + + // in order to ensure NM does not resubmit results, we keep metadata + // of the latest submitted information + // this requires them to submit everything sorted by node_id + pub(crate) submission_metadata: Map<&'static Addr, NetworkMonitorSubmissionMetadata>, +} + +impl PerformanceResultsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + PerformanceResultsStorage { + results: Map::new(storage_keys::PERFORMANCE_RESULTS), + submission_metadata: Map::new(storage_keys::SUBMISSION_METADATA), + } + } + + // note: this method assumes authorisation has been checked and invariants validated + // (such as attempting to insert stale data) + fn insert_performance_data( + &self, + storage: &mut dyn Storage, + epoch_id: EpochId, + data: &NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + let performance = data.performance; + + let key = (epoch_id, data.node_id); + let updated = match self.results.may_load(storage, key)? { + None => NodeResults::new(performance), + Some(mut existing) => { + existing.insert_new(performance); + existing + } + }; + + self.results.save(storage, key, &updated)?; + Ok(()) + } + + fn update_submission_metadata( + &self, + storage: &mut dyn Storage, + address: &Addr, + last_submitted_epoch_id: EpochId, + last_submitted_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.submission_metadata.save( + storage, + address, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id, + last_submitted_node_id, + }, + )?; + Ok(()) + } + + fn ensure_non_stale_submission( + &self, + storage: &dyn Storage, + address: &Addr, + epoch_id: EpochId, + new_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let last_submission = self.submission_metadata.load(storage, address)?; + + // we can't submit data for past epochs + if last_submission.last_submitted_epoch_id > epoch_id { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + + // if we're submitting for the same epoch, the node id has to be greater than the previous one + if last_submission.last_submitted_epoch_id == epoch_id + && last_submission.last_submitted_node_id >= new_node_id + { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + // if we're submitting for new epoch, node id doesn't matter + Ok(()) + } +} + +pub mod retrieval_limits { + pub const NODE_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_EPOCH_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT: u32 = 50; + pub const NODE_EPOCH_MEASUREMENTS_MAX_LIMIT: u32 = 100; + + pub const NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const RETIRED_NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const EPOCH_PERFORMANCE_PURGE_LIMIT: usize = 200; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod performance_contract_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt, PreInitContract}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[cfg(test)] + mod initialisation { + use super::*; + use nym_contracts_common_testing::{ArbitraryContractStorageWriter, FullReader}; + + fn initialise_storage( + pre_init: &mut PreInitContract, + admin: Option<Addr>, + ) -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = admin.unwrap_or(pre_init.addr_make("admin")); + let deps = pre_init.deps_mut(); + + storage.initialise(deps, env, admin, mixnet_contract.clone(), Vec::new())?; + Ok(()) + } + + #[test] + fn sets_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let admin1 = pre_init.api.addr_make("first-admin"); + let admin2 = pre_init.api.addr_make("second-admin"); + + initialise_storage(&mut pre_init, Some(admin1.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin1).is_ok()); + + let mut pre_init = PreInitContract::new(); + initialise_storage(&mut pre_init, Some(admin2.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin2).is_ok()); + + Ok(()) + } + + #[test] + fn sets_provided_mixnet_contract_address() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + + let expected_mixnet_contract_address = pre_init.mixnet_contract_address.clone(); + let deps = pre_init.deps(); + let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?; + assert_eq!(expected_mixnet_contract_address, mixnet_contract); + Ok(()) + } + + #[test] + fn retrieves_initial_epoch_id_from_mixnet_contract() -> anyhow::Result<()> { + // base case + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(0, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + // non-0 epoch + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let address = pre_init.mixnet_contract_address.clone(); + + // advance the epoch few times... + let interval_details = pre_init + .querier() + .query_current_mixnet_interval(&address)? + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch(); + + pre_init.set_contract_storage_value(&address, b"ci", &interval_details)?; + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(7, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + Ok(()) + } + + #[test] + fn authorises_provided_network_monitors() -> anyhow::Result<()> { + // no NM + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 0); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + assert!(authorised.is_empty()); + + let mut pre_init = PreInitContract::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = pre_init.addr_make("admin"); + let nm1 = pre_init.addr_make("nm1"); + let nm2 = pre_init.addr_make("nm2"); + + let deps = pre_init.deps_mut(); + storage.initialise( + deps, + env.clone(), + admin.clone(), + mixnet_contract.clone(), + vec![nm1.to_string(), nm2.to_string()], + )?; + + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 2); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + + let expected = vec![ + NetworkMonitorDetails { + address: nm1, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + NetworkMonitorDetails { + address: nm2, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + ]; + assert_eq!(authorised, expected); + + Ok(()) + } + } + + #[test] + fn getting_current_mixnet_epoch_id() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 0); + tester.advance_mixnet_epoch()?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1); + + tester.set_mixnet_epoch(1000)?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1000); + + Ok(()) + } + + #[cfg(test)] + mod submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + + tester.authorise_network_monitor(&nm1)?; + + // authorised network monitor can submit the results just fine + let perf = tester.dummy_node_performance(); + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm1, 0, perf) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .submit_performance_data(tester.deps_mut(), &unauthorised, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .is_ok()); + + // second submission + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 1, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 10, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 9, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + tester.set_mixnet_epoch(10)?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let perf = tester.dummy_node_performance(); + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 9, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 10, perf) + .is_ok()); + assert!(storage + .submit_performance_data(tester.deps_mut(), &nm, 11, perf) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + storage.submit_performance_data( + tester.deps_mut(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + storage.submit_performance_data( + tester.deps_mut(), + &nm, + 0, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[3]); + + storage.submit_performance_data( + tester.deps_mut(), + &nm, + 1, + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + storage.submit_performance_data( + tester.deps_mut(), + &nm, + 12345, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 12345); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn requires_associated_node_to_be_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let dummy_perf = NodePerformance { + node_id: 12345, + performance: Percent::from_percentage_value(69)?, + }; + + // no node bonded at this point + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded nym-node + let node_id = tester.bond_dummy_nymnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_nymnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded legacy mix-node + let node_id = tester.bond_dummy_legacy_mixnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_legacy_mixnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod batch_submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + + tester.authorise_network_monitor(&nm1)?; + + let perf = tester.dummy_node_performance(); + // authorised network monitor can submit the results just fine + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm1, 0, vec![perf]) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + &unauthorised, + 0, + vec![perf], + ) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn requires_sorted_list_of_performances() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + let more_data = NodePerformance { + node_id: id3, + performance: Percent::hundred(), + }; + + let duplicates = vec![data, data]; + let another_dups = vec![another_data, another_data]; + let unsorted = vec![another_data, data]; + let semi_sorted = vec![data, more_data, another_data]; + let sorted = vec![data, another_data, more_data]; + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, duplicates) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, another_dups) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, unsorted) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, semi_sorted) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, sorted) + .is_ok()); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .is_ok()); + + // second submission + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 1, vec![data]) + .is_ok()); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .is_ok()); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![data]) + .is_ok()); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![data]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + tester.set_mixnet_epoch(10)?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let perf = tester.dummy_node_performance(); + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![perf]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![perf]) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![perf]) + .is_ok()); + assert!(storage + .batch_submit_performance_results(tester.deps_mut(), &nm, 11, vec![perf]) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[4]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 2); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn informs_if_associated_node_is_not_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + // bond and unbond some nodes to advance the id counter + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(node_id)?; + } + + let nym_node1 = tester.bond_dummy_nymnode()?; + + let nym_node_between = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(nym_node_between)?; + + let nym_node2 = tester.bond_dummy_nymnode()?; + + let mix_node1 = tester.bond_dummy_legacy_mixnode()?; + + let mixnode_between = tester.bond_dummy_legacy_mixnode()?; + tester.unbond_legacy_mixnode(mixnode_between)?; + + let mix_node2 = tester.bond_dummy_legacy_mixnode()?; + + // single id - nothing bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 0, + vec![NodePerformance { + node_id: 999999, + performance: Default::default(), + }], + )?; + assert_eq!(res.accepted_scores, 0); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // one bonded nym-node, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 2, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, nym_node_between]); + + // MIXNODES + + // one bonded mixnode, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 3, + vec![ + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 4, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: mixnode_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, mixnode_between]); + + // nym-node, not bonded, mixnode + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + &nm, + 5, + vec![ + NodePerformance { + node_id: 3, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![3, nym_node_between]); + + Ok(()) + } + } + + #[test] + fn checking_for_admin() -> anyhow::Result<()> { + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let storage = NymPerformanceContractStorage::new(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.is_admin(deps, &admin)?); + assert!(!storage.is_admin(deps, &non_admin)?); + + Ok(()) + } + + #[test] + fn ensuring_admin_privileges() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin).is_ok()); + assert!(storage.ensure_is_admin(deps, &non_admin).is_err()); + + Ok(()) + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + let another_nm = tester.addr_make("another-network-monitor"); + + // old one no longer works + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, another_nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &new_admin, another_nm) + .is_ok()); + Ok(()) + } + + #[test] + fn network_monitor_must_not_already_be_authorised() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone()) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::AlreadyAuthorised { address: nm } + ); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `insert_new` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + Ok(()) + } + + #[test] + fn initial_metadata_uses_current_mixnet_epoch() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + assert_eq!( + 0, + storage + .performance_results + .submission_metadata + .load(&tester, &nm1)? + .last_submitted_epoch_id + ); + + tester.advance_mixnet_epoch()?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + assert_eq!( + 1, + storage + .performance_results + .submission_metadata + .load(&tester, &nm2)? + .last_submitted_epoch_id + ); + + tester.set_mixnet_epoch(1000)?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + assert_eq!( + 1000, + storage + .performance_results + .submission_metadata + .load(&tester, &nm3)? + .last_submitted_epoch_id + ); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let another_nm = tester.addr_make("another-network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + storage.authorise_network_monitor( + tester.deps_mut(), + &env, + &admin, + another_nm.clone(), + )?; + + let res = storage + .retire_network_monitor(tester.deps_mut(), env.clone(), ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env.clone(), &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + another_nm.clone(), + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env, &new_admin, another_nm) + .is_ok()); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `retire` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + storage.retire_network_monitor(tester.deps_mut(), env, &admin, nm)?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + Ok(()) + } + } + + #[test] + fn loading_performance_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let admin = tester.admin_unchecked(); + let mut nms = Vec::new(); + + // pre-authorise some network monitors + for i in 0..6 { + let env = tester.env(); + let nm = tester.addr_make(&format!("network-monitor{i}")); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + nms.push(nm); + } + + // no results + let node_id = tester.bond_dummy_nymnode()?; + assert_eq!(storage.try_load_performance(&tester, 0, node_id)?, None); + + // + // always returns median value with 2decimal places precision + // + + // single result + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.42")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.42" + ); + + // two results (median doesn't require changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.50")?; + tester.insert_raw_performance(&nms[1], node_id, "0.40")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.45" + ); + + // two results (median requires changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.58")?; + tester.insert_raw_performance(&nms[1], node_id, "0.45")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.52" + ); + + // three results (median is the middle value rather than the average) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.12")?; + tester.insert_raw_performance(&nms[1], node_id, "0.34")?; + tester.insert_raw_performance(&nms[2], node_id, "0.56")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.34" + ); + + // five results (notice how they're not inserted sorted) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.1")?; + tester.insert_raw_performance(&nms[5], node_id, "0.7")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.7" + ); + + // six results (same as above, but average of middle values) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[3], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.2")?; + tester.insert_raw_performance(&nms[5], node_id, "0.3")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.25" + ); + + Ok(()) + } + + #[cfg(test)] + mod removing_node_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_node_measurements(tester.deps_mut(), ¬_admin, epoch_id, id1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &new_admin, epoch_id, id2) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_if_entry_didnt_exist() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + let node_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, node_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + + // single measurement + tester.insert_raw_performance(&nm1, id1, "0.42")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(after.is_none()); + + // multiple measurements + tester.insert_raw_performance(&nm1, id2, "0.42")?; + tester.insert_raw_performance(&nm2, id2, "0.69")?; + tester.insert_raw_performance(&nm3, id2, "1")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(after.is_none()); + + Ok(()) + } + } + + #[cfg(test)] + mod removing_epoch_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + // epoch 0 + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + // epoch 1 + tester.advance_mixnet_epoch()?; + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_epoch_measurements(tester.deps_mut(), ¬_admin, 0) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 0) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &new_admin, 1) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_for_empty_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data_below_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!(before.len(), 10); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + // EXACT limit + let epoch_id = 1; + tester.advance_mixnet_epoch()?; + for _ in 0..retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn indicates_need_for_further_calls_above_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!( + before.len(), + 2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!( + after.len(), + retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!(after.len(), 50); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + } + } + + #[cfg(test)] + mod network_monitors_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + let main_storage = NymPerformanceContractStorage::new(); + + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + // total authorised count is incremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // its current data is saved + assert_eq!( + storage.authorised.load(&tester, &nm1)?, + NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm2) + .is_ok()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + assert_eq!( + storage.authorised.load(&tester, &nm2)?, + NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + main_storage.retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + nm1.clone(), + )?; + assert!(storage.retired.may_load(&tester, &nm1)?.is_some()); + + // if it was previously retired, that information is purged + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + assert!(storage.retired.may_load(&tester, &nm1)?.is_none()); + + Ok(()) + } + + #[test] + fn retiring_existing_monitor() -> anyhow::Result<()> { + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + tester.authorise_network_monitor(&nm1)?; + tester.authorise_network_monitor(&nm2)?; + + // fails on unauthorised NMs + assert!(storage + .retire(tester.deps_mut(), &env, &admin, &nm3) + .is_err()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + + storage.retire(tester.deps_mut(), &env, &admin, &nm1)?; + + // total authorised count is decremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // data is removed + assert!(storage.authorised.may_load(&tester, &nm1)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm1)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + storage.retire(tester.deps_mut(), &env, &admin, &nm2)?; + + assert_eq!(storage.authorised_count.load(&tester)?, 0); + assert!(storage.authorised.may_load(&tester, &nm2)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm2)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + Ok(()) + } + } + + #[cfg(test)] + mod performance_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::ContractOpts; + use std::str::FromStr; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + // essentially make sure there are no silly bugs that epoch_id and node_id got accidentally mixed up + // when constructing map key... + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let node_id1 = 123; + let node_id2 = 456; + + let data1 = NodePerformance { + node_id: node_id1, + performance: Percent::from_str("0.23")?, + }; + + let data2 = NodePerformance { + node_id: node_id1, + performance: Percent::hundred(), + }; + + let data3 = NodePerformance { + node_id: node_id2, + performance: Percent::from_str("0.23643634")?, + }; + + let data4 = NodePerformance { + node_id: node_id2, + performance: Percent::hundred(), + }; + + assert!(storage.results.may_load(&tester, (1, node_id1))?.is_none()); + assert!(storage.results.may_load(&tester, (1, node_id2))?.is_none()); + + storage.insert_performance_data(&mut tester, 1, &data1)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance] + ); + storage.insert_performance_data(&mut tester, 1, &data2)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 1, &data3)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[data3.performance.round_to_two_decimal_places()] + ); + storage.insert_performance_data(&mut tester, 1, &data4)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[ + data3.performance.round_to_two_decimal_places(), + data4.performance + ] + ); + + storage.insert_performance_data(&mut tester, 2, &data2)?; + storage.insert_performance_data(&mut tester, 2, &data2)?; + assert_eq!( + tester.read_raw_scores(2, node_id1)?.inner(), + &[data2.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 2, &data4)?; + storage.insert_performance_data(&mut tester, 2, &data4)?; + assert_eq!( + tester.read_raw_scores(2, node_id2)?.inner(), + &[data4.performance, data4.performance] + ); + + Ok(()) + } + + #[test] + fn checking_for_submission_staleness() -> anyhow::Result<()> { + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + tester.insert_epoch_performance(&nm, 2, id2, Percent::hundred())?; + + // illegal to submit anything < than last used epoch + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 0, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id3) + .is_err()); + + // for the current epoch, node id has to be greater than what has already been submitted + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id1) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id3) + .is_ok()); + + // and anything for future epochs is fine (as long as it's the first entry) + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id1) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id2) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1111, id3) + .is_ok()); + + Ok(()) + } + } +} diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs new file mode 100644 index 0000000000..c1295d38a5 --- /dev/null +++ b/contracts/performance/src/testing/mod.rs @@ -0,0 +1,606 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::{execute, instantiate, migrate, query}; +use crate::helpers::MixnetContractQuerier; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::testing::{message_info, mock_env, MockApi}; +use cosmwasm_std::{ + coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, + StdError, StdResult, +}; +use cw_storage_plus::PrimaryKey; +use mixnet_contract::testable_mixnet_contract::MixnetContract; +use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, + ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester, + ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract, + TEST_DENOM, +}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; +use nym_mixnet_contract_common::{ + CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse, + NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment, + SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, QueryMsg, +}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +pub struct PerformanceContract; + +impl TestableNymContract for PerformanceContract { + const NAME: &'static str = "performance-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPerformanceContractError; + + fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> { + instantiate + } + + fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> { + execute + } + + fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> { + query + } + + fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + mixnet_contract_address: addr("mixnet-contract").to_string(), + authorised_network_monitors: vec![], + } + } + + fn init() -> ContractTester<Self> + where + Self: Sized, + { + let builder = ContractTesterBuilder::new().instantiate::<MixnetContract>(None); + + // we just instantiated it + let mixnet_address = builder + .well_known_contracts + .get(MixnetContract::NAME) + .unwrap() + .clone(); + + builder + .instantiate::<Self>(Some(InstantiateMsg { + mixnet_contract_address: mixnet_address.to_string(), + authorised_network_monitors: vec![], + })) + .build() + } +} + +pub fn init_contract_tester() -> ContractTester<PerformanceContract> { + PerformanceContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) +} + +// we need to be able to test instantiation, but for that we require +// deps in a state that already includes instantiated mixnet contract +pub(crate) struct PreInitContract { + tester_builder: ContractTesterBuilder<PerformanceContract>, + pub(crate) mixnet_contract_address: Addr, + pub(crate) api: MockApi, + storage: ContractStorageWrapper, + placeholder_address: Addr, +} + +#[allow(dead_code)] +impl PreInitContract { + pub(crate) fn new() -> PreInitContract { + let tester_builder = + ContractTesterBuilder::<PerformanceContract>::new().instantiate::<MixnetContract>(None); + + let mixnet_contract = tester_builder + .well_known_contracts + .get(&MixnetContract::NAME) + .unwrap(); + + let api = tester_builder.api(); + let placeholder_address = api.addr_make("to-be-performance-contract"); + + let storage = tester_builder.contract_storage_wrapper(&placeholder_address); + + PreInitContract { + mixnet_contract_address: mixnet_contract.clone(), + tester_builder, + api, + storage, + placeholder_address, + } + } + + pub(crate) fn deps(&self) -> Deps { + Deps { + storage: &self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn deps_mut(&mut self) -> DepsMut { + DepsMut { + storage: &mut self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn querier(&self) -> QuerierWrapper { + self.tester_builder.querier() + } + + pub(crate) fn env(&self) -> Env { + Env { + contract: ContractInfo { + address: self.placeholder_address.clone(), + }, + ..mock_env() + } + } + + pub(crate) fn addr_make(&self, input: &str) -> Addr { + self.api.addr_make(input) + } + + pub(crate) fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage(address, key, value); + Ok(()) + } + + pub(crate) fn write_to_mixnet_contract_storage_value<T: Serialize>( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage_value(address, key, value) + } +} + +impl ArbitraryContractStorageWriter for PreInitContract { + fn set_contract_storage( + &mut self, + address: impl Into<String>, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} + +#[allow(dead_code)] +pub(crate) trait PerformanceContractTesterExt: + ContractOpts< + ExecuteMsg = ExecuteMsg, + QueryMsg = QueryMsg, + ContractError = NymPerformanceContractError, + > + ChainOpts + + AdminExt + + DenomExt + + RandExt + + BankExt + + ArbitraryContractStorageReader + + ArbitraryContractStorageWriter +{ + fn mixnet_contract_address(&self) -> StdResult<Addr> { + NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage) + } + + fn execute_mixnet_contract( + &mut self, + sender: MessageInfo, + msg: &nym_mixnet_contract_common::ExecuteMsg, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.execute_arbitrary_contract(address, sender, msg) + .map_err(|err| { + StdError::generic_err(format!("mixnet contract execution failure: {err}")) + })?; + Ok(()) + } + + fn read_from_mixnet_contract_storage<T: DeserializeOwned>( + &self, + key: impl AsRef<[u8]>, + ) -> StdResult<T> { + let address = self.mixnet_contract_address()?; + + self.must_read_value_from_contract_storage(address, key) + } + + fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + <Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value); + Ok(()) + } + + fn write_to_mixnet_contract_storage_value<T: Serialize>( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.set_contract_storage_value(address, key, value) + } + + fn current_mixnet_epoch(&self) -> StdResult<EpochId> { + let address = self.mixnet_contract_address()?; + + Ok(self + .deps() + .querier + .query_current_mixnet_interval(address.clone())? + .current_epoch_absolute_id()) + } + + fn advance_mixnet_epoch(&mut self) -> StdResult<()> { + let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetCurrentIntervalDetails {}, + )?; + let until_end = interval_details.time_until_current_epoch_end().as_secs(); + let timestamp = self.env().block.time.plus_seconds(until_end + 1); + self.set_block_time(timestamp); + self.next_block(); + + // this was hardcoded in mixnet init + let mixnet_rewarder = self.addr_make("rewarder"); + let rewarder = message_info(&mixnet_rewarder, &[]); + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {}, + )?; + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None }, + )?; + + for role in [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role, + nodes: vec![], + }, + }, + )?; + } + Ok(()) + } + + fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + let interval = self + .deps() + .querier + .query_current_mixnet_interval(address.clone())?; + + let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id { + interval + } else { + Interval::init_interval( + interval.epochs_in_interval(), + interval.epoch_length(), + &mock_env(), + ) + }; + + let current = to_update.current_epoch_absolute_id(); + let diff = epoch_id - current; + for _ in 0..diff { + to_update = to_update.advance_epoch(); + } + self.set_contract_storage_value(&address, b"ci", &to_update) + } + + fn authorise_network_monitor( + &mut self, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::AuthoriseNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn dummy_node_performance(&mut self) -> NodePerformance { + let node_id = self.bond_dummy_nymnode().unwrap(); + NodePerformance { + node_id, + performance: Percent::from_percentage_value(69).unwrap(), + } + } + + fn retire_network_monitor(&mut self, addr: &Addr) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::RetireNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn insert_epoch_performance( + &mut self, + addr: &Addr, + epoch_id: EpochId, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + self.deps_mut(), + addr, + epoch_id, + NodePerformance { + node_id, + performance, + }, + ) + } + + fn insert_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + let epoch_id = self.current_mixnet_epoch()?; + + self.insert_epoch_performance(addr, epoch_id, node_id, performance) + } + + // makes testing easier + fn insert_raw_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + raw: &str, + ) -> Result<(), NymPerformanceContractError> { + self.insert_performance( + addr, + node_id, + Percent::from_str(raw).map_err(|err| { + NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string())) + })?, + ) + } + + fn read_raw_scores( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<NodeResults, NymPerformanceContractError> { + let scores = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .load(self.deps().storage, (epoch_id, node_id))?; + Ok(scores) + } + + fn bond_dummy_nymnode(&mut self) -> Result<NodeId, NymPerformanceContractError> { + let node_owner = self.generate_account_with_balance(); + let pledge = coins(100_000000, TEST_DENOM); + let keypair = ed25519::KeyPair::new(self.raw_rng()); + let identity_key = keypair.public_key().to_base58_string(); + + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, + identity_key, + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + // initial signing nonce is 0 for a new address + let signing_nonce = 0; + + let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone()); + let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload); + let msg = SignableNymNodeBondingMsg::new(signing_nonce, content); + + let owner_signature = keypair.private_key().sign(msg.to_plaintext()?); + let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref()); + + self.execute_mixnet_contract( + message_info(&node_owner, &pledge), + &nym_mixnet_contract_common::ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + )?; + + let bond: NodeOwnershipResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetOwnedNymNode { + address: node_owner.to_string(), + }, + )?; + + Ok(bond.details.unwrap().bond_information.node_id) + } + + fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), NymPerformanceContractError> { + let bond: NodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetNymNodeDetails { node_id }, + )?; + + let node_owner = bond.details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondNymNode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } + + fn bond_dummy_legacy_mixnode(&mut self) -> Result<NodeId, NymPerformanceContractError> { + #[derive(Deserialize, Serialize)] + pub(crate) struct UniqueRef<T> { + // note, we collapse the pk - combining everything under the namespace - even if it is composite + pk: Binary, + value: T, + } + + // there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit, + // ensuring all invariants still hold + let owner = self.generate_account_with_balance(); + + let mixnode = MixNode { + host: "1.2.3.4".to_string(), + mix_port: 123, + verloc_port: 123, + http_api_port: 123, + sphinx_key: "aaaa".to_string(), + identity_key: "bbbbb".to_string(), + version: "ccc".to_string(), + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + + // adjust node counter + let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?; + let node_id = node_id_counter + 1; + self.write_to_mixnet_contract_storage_value("nic", &node_id)?; + + let current_epoch = self.current_mixnet_epoch()?; + let pledge = coin(100_000000, TEST_DENOM); + let mixnode_rewarding = + NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap(); + let env = self.env(); + let mixnode_bond = MixNodeBond { + mix_id: node_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save to the main mixnode storage + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnn", + node_id, + &mixnode_bond, + )?; + // update indices + let pk = node_id.joined_key(); + let unique_ref = UniqueRef { + pk: pk.into(), + value: mixnode_bond.clone(), + }; + + // owner index + let idx = mixnode_bond.owner.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?; + + // identity key index + let idx = mixnode_bond.mix_node.identity_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?; + + // sphinx key index + let idx = mixnode_bond.mix_node.sphinx_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?; + + // update rewarding data + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnr", + node_id, + &mixnode_rewarding, + )?; + + Ok(node_id) + } + + fn unbond_legacy_mixnode( + &mut self, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let bond: MixnodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id }, + )?; + + let node_owner = bond.mixnode_details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } +} + +impl PerformanceContractTesterExt for ContractTester<PerformanceContract> {} diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs new file mode 100644 index 0000000000..6385661fa3 --- /dev/null +++ b/contracts/performance/src/transactions.rs @@ -0,0 +1,305 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response}; +use nym_performance_contract_common::{ + EpochId, NodeId, NodePerformance, NymPerformanceContractError, +}; + +pub fn try_update_contract_admin( + deps: DepsMut<'_>, + info: MessageInfo, + new_admin: String, +) -> Result<Response, NymPerformanceContractError> { + let new_admin = deps.api.addr_validate(&new_admin)?; + + let res = NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .execute_update_admin(deps, info, Some(new_admin))?; + + Ok(res) +} + +pub fn try_submit_performance_results( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, + data: NodePerformance, +) -> Result<Response, NymPerformanceContractError> { + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(deps, &info.sender, epoch_id, data)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_batch_submit_performance_results( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, + data: Vec<NodePerformance>, +) -> Result<Response, NymPerformanceContractError> { + let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( + deps, + &info.sender, + epoch_id, + data, + )?; + + let response = Response::new().set_data(to_json_binary(&res)?).add_event( + Event::new("batch_performance_submission") + .add_attribute("accepted_scores", res.accepted_scores.to_string()) + .add_attribute( + "non_existent_nodes", + format!("{:?}", res.non_existent_nodes), + ), + ); + Ok(response) +} + +pub fn try_authorise_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result<Response, NymPerformanceContractError> { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.authorise_network_monitor( + deps, + &env, + &info.sender, + address, + )?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_retire_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result<Response, NymPerformanceContractError> { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.retire_network_monitor(deps, env, &info.sender, address)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_remove_node_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, + node_id: NodeId, +) -> Result<Response, NymPerformanceContractError> { + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_node_measurements( + deps, + &info.sender, + epoch_id, + node_id, + )?; + + Ok(Response::new()) +} + +pub fn try_remove_epoch_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, +) -> Result<Response, NymPerformanceContractError> { + let res = + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_epoch_measurements(deps, &info.sender, epoch_id)?; + + Ok(Response::new().set_data(to_json_binary(&res)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::retrieval_limits; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use cosmwasm_std::from_json; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + use nym_performance_contract_common::RemoveEpochMeasurementsResponse; + + #[cfg(test)] + mod updating_contract_admin { + use super::*; + use crate::testing::init_contract_tester; + use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let random_acc = test.generate_account(); + let new_admin = test.generate_account(); + let res = test + .execute_raw( + random_acc, + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::Admin(AdminError::NotAdmin {}) + ); + + let actual_admin = test.admin_unchecked(); + let res = test.execute_raw( + actual_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ); + assert!(res.is_ok()); + + let updated_admin = test.admin_unchecked(); + assert_eq!(new_admin, updated_admin); + + Ok(()) + } + + #[test] + fn requires_providing_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_account = "definitely-not-valid-account"; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: bad_account.to_string(), + }, + ); + + assert!(res.is_err()); + + let empty_account = ""; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: empty_account.to_string(), + }, + ); + + assert!(res.is_err()); + + Ok(()) + } + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_authorise_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_authorise_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + test.authorise_network_monitor(&good_address)?; + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_retire_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_retire_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + // panics in tests are fine... + #[allow(clippy::panic)] + #[test] + fn removing_epoch_measurements_returns_binary_data() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + tester.advance_mixnet_epoch()?; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let admin = tester.admin_msg(); + let res = try_remove_epoch_measurements(tester.deps_mut(), admin.clone(), 0)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(!deserialised.additional_entries_to_remove_remaining); + + let res = try_remove_epoch_measurements(tester.deps_mut(), admin, 1)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(deserialised.additional_entries_to_remove_remaining); + + Ok(()) + } +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 24f918e136..2a3692d056 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4246,7 +4246,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -4325,6 +4324,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-serde-helpers" version = "0.1.0" @@ -4432,6 +4444,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-performance-contract-common", "nym-serde-helpers", "nym-vesting-contract-common", "prost", diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index b35e56ad70..f8e66ebe3e 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -24,6 +24,10 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh"; +// \/ TODO: this has to be updated once the contract is deployed +pub(crate) const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + // -- Constructor functions -- pub(crate) fn validators() -> Vec<ValidatorDetails> { @@ -46,6 +50,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str(PERFORMANCE_CONTRACT_ADDRESS), ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml index d5384a9953..00e2bd19ce 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/testnet-manager/Cargo.toml @@ -45,6 +45,7 @@ nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/g nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-performance-contract-common = { path = "../../../common/cosmwasm-smart-contracts/nym-performance-contract" } nym-pemstore = { path = "../../../common/pemstore" } diff --git a/tools/internal/testnet-manager/migrations/02_performance_contract.sql b/tools/internal/testnet-manager/migrations/02_performance_contract.sql new file mode 100644 index 0000000000..ad8d53b2c5 --- /dev/null +++ b/tools/internal/testnet-manager/migrations/02_performance_contract.sql @@ -0,0 +1,53 @@ +/* + * Copyright 2025 - Nym Technologies SA <contact@nymtech.net> + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE network_old +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +INSERT INTO network_old +SELECT * +from network; + +DROP TABLE network; + +CREATE TABLE network +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + performance_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE TABLE authorised_network_monitor +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + network_id INTEGER NOT NULL REFERENCES network (id), + address TEXT NOT NULL REFERENCES account (address) +); diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 8295550dfc..6328dc0b10 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -18,6 +18,7 @@ pub(crate) struct LoadedNymContracts { pub(crate) cw3_multisig: LoadedContract, pub(crate) cw4_group: LoadedContract, pub(crate) dkg: LoadedContract, + pub(crate) performance: LoadedContract, } impl From<NymContracts> for LoadedNymContracts { @@ -29,6 +30,7 @@ impl From<NymContracts> for LoadedNymContracts { cw3_multisig: value.cw3_multisig.into(), cw4_group: value.cw4_group.into(), dkg: value.dkg.into(), + performance: value.performance.into(), } } } @@ -41,6 +43,7 @@ pub(crate) struct NymContracts { pub(crate) cw3_multisig: Contract, pub(crate) cw4_group: Contract, pub(crate) dkg: Contract, + pub(crate) performance: Contract, } impl NymContracts { @@ -52,6 +55,7 @@ impl NymContracts { &self.cw3_multisig, &self.cw4_group, &self.dkg, + &self.performance, ] } @@ -63,11 +67,12 @@ impl NymContracts { &mut self.cw3_multisig, &mut self.cw4_group, &mut self.dkg, + &mut self.performance, ] } pub(crate) fn count(&self) -> usize { - 6 + 7 } pub(crate) fn discover_paths<P: AsRef<Path>>( @@ -100,6 +105,9 @@ impl NymContracts { if name.contains("dkg") { self.dkg.wasm_path = Some(entry.path()) } + if name.contains("performance") { + self.performance.wasm_path = Some(entry.path()) + } } } @@ -122,6 +130,7 @@ impl Default for NymContracts { cw4_group: Contract::new("cw4_group"), cw3_multisig: Contract::new("cw3_multisig"), dkg: Contract::new("dkg"), + performance: Contract::new("performance"), } } } diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index d5b6204a05..39e04f10cf 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -65,6 +65,7 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { let contracts = nym_config::defaults::NymContracts { mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + performance_contract_address: Some(value.contracts.performance.address.to_string()), ecash_contract_address: Some(value.contracts.ecash.address.to_string()), group_contract_address: Some(value.contracts.cw4_group.address.to_string()), multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), @@ -134,6 +135,7 @@ impl LoadedNetwork { pub struct SpecialAddresses { pub ecash_holding_account: Account, pub mixnet_rewarder: Account, + pub network_monitors: Vec<Account>, } impl Default for SpecialAddresses { @@ -141,6 +143,8 @@ impl Default for SpecialAddresses { SpecialAddresses { ecash_holding_account: Account::new(), mixnet_rewarder: Account::new(), + // by default use one address; to be adjusted in the future + network_monitors: vec![Account::new()], } } } diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index ddd8a37f56..44c5a97335 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -261,6 +261,22 @@ impl NetworkManager { }) } + fn performance_init_message( + &self, + ctx: &InitCtx, + ) -> Result<nym_performance_contract_common::msg::InstantiateMsg, NetworkManagerError> { + Ok(nym_performance_contract_common::msg::InstantiateMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + authorised_network_monitors: ctx + .network + .auxiliary_addresses + .network_monitors + .iter() + .map(|acc| acc.address.to_string()) + .collect(), + }) + } + fn find_contracts<P: AsRef<Path>>( &self, ctx: &mut InitCtx, @@ -296,6 +312,10 @@ impl NetworkManager { "\tdiscovered dkg contract at '{}'", ctx.network.contracts.dkg.wasm_path()?.display() )); + ctx.println(format!( + "\tdiscovered performance contract at '{}'", + ctx.network.contracts.performance.wasm_path()?.display() + )); ctx.println("\t✅ found all the contracts!"); @@ -392,6 +412,11 @@ impl NetworkManager { ctx.admin.mix_coins(10_000000), )); + // and to any network monitors + for network_monitor in &ctx.network.auxiliary_addresses.network_monitors { + receivers.push((network_monitor.address(), ctx.admin.mix_coins(10_000000))) + } + ctx.set_pb_message("attempting to send admin tokens..."); let send_future = @@ -553,6 +578,28 @@ impl NetworkManager { )); ctx.network.contracts.ecash.init_info = Some(res.into()); + // performance (semi-temp) + ctx.set_pb_prefix(format!("[7/{total}]")); + let name = &ctx.network.contracts.performance.name; + let code_id = ctx.network.contracts.performance.upload_info()?.code_id; + let admin = ctx.network.contracts.performance.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.performance_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.performance.init_info = Some(res.into()); + ctx.println("\t✅ instantiated all the contracts!"); Ok(()) diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs index 24132db687..1bd3713d3b 100644 --- a/tools/internal/testnet-manager/src/manager/storage/manager.rs +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -1,6 +1,8 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use crate::manager::storage::models::{ + RawAccount, RawAuthorisedNetworkMonitor, RawContract, RawNetwork, +}; use time::OffsetDateTime; #[derive(Clone)] @@ -85,6 +87,7 @@ impl StorageManager { cw3_id: i64, cw4_id: i64, dkg_id: i64, + performance_id: i64, rewarder_address: &str, ecash_holding_address: &str, ) -> Result<i64, sqlx::Error> { @@ -99,10 +102,11 @@ impl StorageManager { cw3_multisig_contract_id, cw4_group_contract_id, dkg_contract_id, + performance_contract_id, rewarder_address, ecash_holding_account_address ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, name, created_at, @@ -112,6 +116,7 @@ impl StorageManager { cw3_id, cw4_id, dkg_id, + performance_id, rewarder_address, ecash_holding_address, ) @@ -159,19 +164,46 @@ impl StorageManager { .await } + pub(crate) async fn save_authorised_network_monitor( + &self, + network_id: i64, + address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)", + network_id, + address, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_authorised_network_monitors( + &self, + network_id: i64, + ) -> Result<Vec<RawAuthorisedNetworkMonitor>, sqlx::Error> { + sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + pub(crate) async fn save_account( &self, address: &str, mnemonic: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( + ) -> Result<i64, sqlx::Error> { + let account_id = sqlx::query!( "INSERT INTO account (address, mnemonic) VALUES (?, ?)", address, mnemonic ) .execute(&self.connection_pool) - .await?; - Ok(()) + .await? + .last_insert_rowid(); + Ok(account_id) } pub(crate) async fn load_account(&self, address: &str) -> Result<RawAccount, sqlx::Error> { diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs index 7eb3df7cb1..21c85df32e 100644 --- a/tools/internal/testnet-manager/src/manager/storage/mod.rs +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -1,6 +1,6 @@ -use std::fs; // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only + use crate::{ error::NetworkManagerError, manager::{ @@ -14,6 +14,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use std::fs; use std::path::Path; use tracing::{error, info}; use url::Url; @@ -159,7 +160,7 @@ impl NetworkManagerStorage { .await?) } - async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + async fn persist_account(&self, account: &Account) -> Result<i64, NetworkManagerError> { let as_str = Zeroizing::new(account.mnemonic.to_string()); Ok(self .manager @@ -191,6 +192,18 @@ impl NetworkManagerStorage { Ok(()) } + async fn persist_authorised_network_monitor( + &self, + network_id: i64, + account: &Account, + ) -> Result<(), NetworkManagerError> { + self.persist_account(account).await?; + self.manager + .save_authorised_network_monitor(network_id, account.address.as_ref()) + .await?; + Ok(()) + } + pub(crate) async fn persist_network( &self, network: &Network, @@ -206,6 +219,8 @@ impl NetworkManagerStorage { self.persist_account(network.contracts.cw4_group.admin()?) .await?; self.persist_account(network.contracts.dkg.admin()?).await?; + self.persist_account(network.contracts.performance.admin()?) + .await?; self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) .await?; @@ -220,6 +235,9 @@ impl NetworkManagerStorage { .await?; let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + let performance_id = self + .persist_contract(&network.contracts.performance) + .await?; let network_id = self .manager @@ -232,6 +250,7 @@ impl NetworkManagerStorage { cw3_multisig_id, cw4_group_id, dkg_id, + performance_id, network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), network .auxiliary_addresses @@ -242,6 +261,10 @@ impl NetworkManagerStorage { .await?; self.manager.save_latest_network_id(network_id).await?; + for nm in &network.auxiliary_addresses.network_monitors { + self.persist_authorised_network_monitor(network_id, nm) + .await? + } Ok(()) } @@ -256,6 +279,20 @@ impl NetworkManagerStorage { .await? .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + let authorised = self + .manager + .load_authorised_network_monitors(base_network.id) + .await?; + let mut network_monitors = Vec::with_capacity(authorised.len()); + for authorised in authorised { + network_monitors.push( + self.manager + .load_account(&authorised.address) + .await? + .try_into()?, + ) + } + Ok(LoadedNetwork { id: base_network.id, name: base_network.name, @@ -292,6 +329,11 @@ impl NetworkManagerStorage { .load_contract(base_network.dkg_contract_id) .await? .try_into()?, + performance: self + .manager + .load_contract(base_network.performance_contract_id) + .await? + .try_into()?, }, auxiliary_addresses: SpecialAddresses { ecash_holding_account: self @@ -304,6 +346,7 @@ impl NetworkManagerStorage { .load_account(&base_network.rewarder_address) .await? .try_into()?, + network_monitors, }, }) } diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs index c224fc36d5..9df7ebb481 100644 --- a/tools/internal/testnet-manager/src/manager/storage/models.rs +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -6,6 +6,14 @@ use crate::manager::contract::{Account, LoadedContract}; use sqlx::FromRow; use time::OffsetDateTime; +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawAuthorisedNetworkMonitor { + pub(crate) id: i64, + pub(crate) network_id: i64, + pub(crate) address: String, +} + #[derive(FromRow)] pub(crate) struct RawAccount { pub(crate) address: String, @@ -70,6 +78,7 @@ pub(crate) struct RawNetwork { pub(crate) cw3_multisig_contract_id: i64, pub(crate) cw4_group_contract_id: i64, pub(crate) dkg_contract_id: i64, + pub(crate) performance_contract_id: i64, pub(crate) rewarder_address: String, pub(crate) ecash_holding_account_address: String, From 07c908c49729ebd06fe45542f89ef85b31b24a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= <bogdan@nymtech.net> Date: Mon, 23 Jun 2025 11:53:39 +0300 Subject: [PATCH 36/47] Return true remaining (#5866) --- .../credential-verification/src/bandwidth_storage_manager.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index c861ef123f..5d51091346 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -88,7 +88,8 @@ impl BandwidthStorageManager { debug!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; - Ok(available_bandwidth) + let remaining_bandwidth = self.client_bandwidth.available().await; + Ok(remaining_bandwidth) } async fn expire_bandwidth(&mut self) -> Result<()> { From eb59615c5660470d62ecf4459407b9f4a6c038e0 Mon Sep 17 00:00:00 2001 From: Simon Wicky <simon@nymtech.net> Date: Mon, 23 Jun 2025 14:58:29 +0200 Subject: [PATCH 37/47] StatsAPI qol : disable swagger try it out and remove debug level from nym_http_api_client (#5868) --- Cargo.lock | 2 +- nym-statistics-api/Cargo.toml | 2 +- nym-statistics-api/src/http/api/mod.rs | 5 +++-- nym-statistics-api/src/logging.rs | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 665a6a4dd0..a18e27146f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7485,7 +7485,7 @@ dependencies = [ [[package]] name = "nym-statistics-api" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "axum 0.7.9", diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml index ae26c96a9e..4d37557118 100644 --- a/nym-statistics-api/Cargo.toml +++ b/nym-statistics-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-statistics-api" -version = "0.1.3" +version = "0.1.4" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-statistics-api/src/http/api/mod.rs b/nym-statistics-api/src/http/api/mod.rs index 9e3fc313d3..16e9763bd0 100644 --- a/nym-statistics-api/src/http/api/mod.rs +++ b/nym-statistics-api/src/http/api/mod.rs @@ -4,7 +4,7 @@ use nym_http_api_common::middleware::logging::log_request_info; use tokio::net::ToSocketAddrs; use tower_http::cors::CorsLayer; use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; +use utoipa_swagger_ui::{Config, SwaggerUi}; use crate::http::{server::HttpServer, state::AppState}; @@ -19,11 +19,12 @@ impl RouterBuilder { let router = Router::new() .merge( SwaggerUi::new("/swagger") + .config(Config::default().supported_submit_methods(["get"])) .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), ) .route( "/", - axum::routing::get(|| async { Redirect::permanent("/swagger") }), + axum::routing::get(|| async { Redirect::permanent("/swagger") }), // SW let's redirect to a blogpost explaining the stats collection process once it exists ) .nest("/v1", Router::new().nest("/stats", stats::routes())); diff --git a/nym-statistics-api/src/logging.rs b/nym-statistics-api/src/logging.rs index 1adfa31b71..0a5757fc08 100644 --- a/nym-statistics-api/src/logging.rs +++ b/nym-statistics-api/src/logging.rs @@ -30,6 +30,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "axum", "reqwest", "hyper_util", + "nym_http_api_client", ]; for crate_name in warn_crates { filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); From 447352b8d6570aa6d679102f4bb6e9fdba7b825d Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:44:06 +0200 Subject: [PATCH 38/47] Set busy_timeout in sqlx (#5872) * Set busy_timeout * Bump version --- Cargo.lock | 2 +- .../nym-node-status-api/Cargo.toml | 2 +- .../nym-node-status-api/src/cli/mod.rs | 4 +++ .../nym-node-status-api/src/db/mod.rs | 32 +++++++++++++++++-- .../nym-node-status-api/src/main.rs | 2 +- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a18e27146f..a1c0b12587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6684,7 +6684,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.1.0" +version = "3.1.1" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 7e63db3672..916f30eb1f 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.1.0" +version = "3.1.1" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index 0a487afe86..f150dbc139 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -45,6 +45,10 @@ pub(crate) struct Cli { #[clap(long, env = "DATABASE_URL")] pub(crate) database_url: String, + #[clap(long, default_value = "5", env = "SQLX_BUSY_TIMEOUT_S")] + #[arg(value_parser = parse_duration)] + pub(crate) sqlx_busy_timeout_s: Duration, + #[clap( long, default_value = "300", diff --git a/nym-node-status-api/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/mod.rs index cc3398964c..3f7d944906 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/mod.rs @@ -1,10 +1,11 @@ use anyhow::{anyhow, Result}; use sqlx::{ migrate::Migrator, + query, sqlite::{SqliteAutoVacuum, SqliteConnectOptions, SqliteSynchronous}, ConnectOptions, SqlitePool, }; -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; pub(crate) mod models; pub(crate) mod queries; @@ -18,9 +19,10 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init(connection_url: String) -> Result<Self> { + pub async fn init(connection_url: String, busy_timeout: Duration) -> Result<Self> { let connect_options = SqliteConnectOptions::from_str(&connection_url)? .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .busy_timeout(busy_timeout) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) .foreign_keys(true) @@ -33,6 +35,9 @@ impl Storage { MIGRATOR.run(&pool).await?; + // aftering setting pragma, check whether it was set successfully + Self::assert_busy_timeout(pool.clone(), busy_timeout.as_secs() as i64).await?; + Ok(Storage { pool }) } @@ -40,4 +45,27 @@ impl Storage { pub fn pool_owned(&self) -> DbPool { self.pool.clone() } + + async fn assert_busy_timeout(pool: DbPool, expected_busy_timeout_s: i64) -> Result<()> { + let mut conn = pool.acquire().await?; + // Sqlite stores this value as miliseconds + // https://www.sqlite.org/pragma.html#pragma_busy_timeout + let busy_timeout_db = query!("PRAGMA busy_timeout;") + .fetch_one(conn.as_mut()) + .await?; + + let actual_busy_timeout_ms = busy_timeout_db.timeout.unwrap_or(0); + tracing::info!("PRAGMA busy_timeout={}ms", actual_busy_timeout_ms); + let expected_busy_timeout_ms = expected_busy_timeout_s * 1000; + + if expected_busy_timeout_ms != actual_busy_timeout_ms { + anyhow::bail!( + "PRAGMA busy_timeout expected: {}ms, actual: {}ms", + expected_busy_timeout_ms, + actual_busy_timeout_ms + ); + } + + Ok(()) + } } diff --git a/nym-node-status-api/nym-node-status-api/src/main.rs b/nym-node-status-api/nym-node-status-api/src/main.rs index f0bd3b5b46..7db3015a7d 100644 --- a/nym-node-status-api/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/nym-node-status-api/src/main.rs @@ -31,7 +31,7 @@ async fn main() -> anyhow::Result<()> { let connection_url = args.database_url.clone(); tracing::debug!("Using config:\n{:#?}", args); - let storage = db::Storage::init(connection_url).await?; + let storage = db::Storage::init(connection_url, args.sqlx_busy_timeout_s).await?; let db_pool = storage.pool_owned(); // Start the node scraper From 658dec829989b3bf388e5623835bfe29252fde4e Mon Sep 17 00:00:00 2001 From: benedettadavico <benedetta.davico@gmail.com> Date: Thu, 26 Jun 2025 12:44:47 +0200 Subject: [PATCH 39/47] fix the broken link --- nym-wallet/src/pages/node-settings/node-stats.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nym-wallet/src/pages/node-settings/node-stats.tsx b/nym-wallet/src/pages/node-settings/node-stats.tsx index 0dfd9ae18b..04b36e7741 100644 --- a/nym-wallet/src/pages/node-settings/node-stats.tsx +++ b/nym-wallet/src/pages/node-settings/node-stats.tsx @@ -4,12 +4,12 @@ import { TauriLink as Link } from 'src/components/TauriLinkWrapper'; import { urls, AppContext } from '../../context/main'; -export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => { +export const NodeStats = ({ identityKey }: { identityKey?: string }) => { const { network } = useContext(AppContext); return ( <Stack spacing={2} sx={{ p: 4 }}> <Typography>All your node stats are available on the link below</Typography> - <Link href={`${urls(network).networkExplorer}/nodes/${mixnodeId}`} target="_blank" text="Network Explorer" /> + <Link href={`${urls(network).networkExplorer}/nodes/${identityKey}`} target="_blank" text="Network Explorer" /> </Stack> ); }; From 2ae38b9e4982d7d917531a7b2c6e3bca8f8141f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Tue, 1 Jul 2025 10:28:57 +0100 Subject: [PATCH 40/47] chore: 1.88 clippy (#5877) * 1.88 clippy * wasm clippy * wallet clippy --- Cargo.lock | 2 + clients/native/src/websocket/handler.rs | 2 +- clients/native/src/websocket/listener.rs | 4 +- common/async-file-watcher/src/lib.rs | 2 +- common/bandwidth-controller/src/event.rs | 2 +- .../client-core/src/client/mix_traffic/mod.rs | 2 +- .../acknowledgement_listener.rs | 2 +- .../action_controller.rs | 26 ++---- .../real_traffic_stream.rs | 2 +- .../sending_delay_controller.rs | 6 +- .../client-core/src/client/received_buffer.rs | 5 +- .../client/replies/reply_controller/mod.rs | 20 ++--- .../src/client/statistics_control.rs | 2 +- .../src/client/transmission_buffer.rs | 2 +- common/client-core/src/init/helpers.rs | 6 +- common/commands/src/utils.rs | 4 +- .../commands/src/validator/account/balance.rs | 2 +- .../commands/src/validator/account/pubkey.rs | 6 +- .../src/validator/account/send_multiple.rs | 15 ++-- .../validator/cosmwasm/execute_contract.rs | 2 +- .../cosmwasm/generators/coconut_dkg.rs | 4 +- .../cosmwasm/generators/ecash_bandwidth.rs | 4 +- .../validator/cosmwasm/generators/mixnet.rs | 6 +- .../validator/cosmwasm/generators/multisig.rs | 4 +- .../validator/cosmwasm/generators/vesting.rs | 4 +- .../src/validator/cosmwasm/init_contract.rs | 2 +- .../validator/cosmwasm/migrate_contract.rs | 2 +- .../src/validator/cosmwasm/upload_contract.rs | 2 +- .../mixnet/delegators/delegate_to_mixnode.rs | 2 +- .../delegate_to_multiple_mixnodes.rs | 10 +-- .../delegators/migrate_vested_delegation.rs | 2 +- .../rewards/claim_delegator_reward.rs | 2 +- .../rewards/vesting_claim_delegator_reward.rs | 2 +- .../delegators/undelegate_from_mixnode.rs | 2 +- .../delegators/vesting_delegate_to_mixnode.rs | 2 +- .../vesting_undelegate_from_mixnode.rs | 2 +- .../mixnet/operators/gateway/bond_gateway.rs | 2 +- .../operators/gateway/nymnode_migration.rs | 2 +- .../gateway/settings/update_config.rs | 2 +- .../gateway/settings/vesting_update_config.rs | 2 +- .../operators/gateway/unbond_gateway.rs | 2 +- .../operators/gateway/vesting_bond_gateway.rs | 2 +- .../gateway/vesting_unbond_gateway.rs | 2 +- .../mixnet/operators/mixnode/bond_mixnode.rs | 2 +- .../operators/mixnode/decrease_pledge.rs | 2 +- .../mixnode/migrate_vested_mixnode.rs | 2 +- .../operators/mixnode/nymnode_migration.rs | 2 +- .../mixnet/operators/mixnode/pledge_more.rs | 2 +- .../mixnode/rewards/claim_operator_reward.rs | 2 +- .../rewards/vesting_claim_operator_reward.rs | 2 +- .../mixnode/settings/update_config.rs | 2 +- .../mixnode/settings/vesting_update_config.rs | 2 +- .../operators/mixnode/unbond_mixnode.rs | 2 +- .../operators/mixnode/vesting_bond_mixnode.rs | 2 +- .../mixnode/vesting_decrease_pledge.rs | 2 +- .../operators/mixnode/vesting_pledge_more.rs | 2 +- .../mixnode/vesting_unbond_mixnode.rs | 2 +- .../mixnet/operators/nymnode/bond_nymnode.rs | 2 +- .../nymnode/pledge/decrease_pledge.rs | 2 +- .../nymnode/pledge/increase_pledge.rs | 2 +- .../nymnode/rewards/claim_operator_reward.rs | 2 +- .../nymnode/settings/update_config.rs | 2 +- .../nymnode/settings/update_cost_params.rs | 2 +- .../operators/nymnode/unbond_nymnode.rs | 2 +- .../commands/src/validator/signature/sign.rs | 4 +- .../src/validator/signature/verify.rs | 9 +- .../vesting/create_vesting_schedule.rs | 4 +- .../vesting/query_vesting_schedule.rs | 2 +- common/credential-utils/src/utils.rs | 2 +- common/gateway-stats-storage/build.rs | 4 +- common/gateway-storage/build.rs | 4 +- common/gateway-storage/src/clients.rs | 2 +- common/http-api-client/src/user_agent.rs | 2 +- common/http-api-common/Cargo.toml | 1 + common/http-api-common/src/response/json.rs | 1 - common/ip-packet-requests/src/v8/request.rs | 2 +- common/network-defaults/build.rs | 10 +-- common/network-defaults/src/env_setup.rs | 2 +- common/network-defaults/src/network.rs | 2 +- common/nym-metrics/src/lib.rs | 6 +- .../src/scheme/setup.rs | 2 +- common/socks5-client-core/src/lib.rs | 4 +- common/socks5-client-core/src/socks/client.rs | 2 +- .../src/socks/mixnet_responses.rs | 4 +- .../proxy-helpers/src/proxy_runner/inbound.rs | 5 +- .../src/proxy_runner/outbound.rs | 6 +- common/socks5/requests/src/request.rs | 2 +- common/socks5/requests/src/response.rs | 2 +- .../src/clients/gateway_conn_statistics.rs | 4 +- .../src/clients/nym_api_statistics.rs | 4 +- .../src/clients/packet_statistics.rs | 4 +- common/statistics/src/lib.rs | 2 +- common/task/src/connections.rs | 2 +- common/task/src/manager.rs | 2 +- common/task/src/signal.rs | 4 +- common/tun/src/linux/tun_device.rs | 2 +- common/wireguard/src/lib.rs | 2 +- documentation/autodoc/src/main.rs | 24 ++---- nym-api/build.rs | 7 +- nym-api/src/ecash/dkg/state/serde_helpers.rs | 4 +- nym-api/src/epoch_operations/helpers.rs | 4 +- nym-api/tests/public-api/network.rs | 8 +- nym-api/tests/public-api/nym_nodes.rs | 2 +- nym-api/tests/public-api/unstable_status.rs | 2 +- nym-api/tests/public-api/utils.rs | 16 ++-- .../nym-credential-proxy/src/storage/mod.rs | 14 ++-- nym-network-monitor/src/accounting.rs | 4 +- nym-network-monitor/src/http.rs | 2 +- nym-network-monitor/src/main.rs | 8 +- .../nym-node-status-agent/src/main.rs | 2 +- .../nym-node-status-agent/src/probe.rs | 8 +- .../nym-node-status-api/build.rs | 5 +- .../nym-node-status-api/src/http/error.rs | 2 +- .../nym-node-status-api/src/http/models.rs | 2 +- .../nym-node-status-api/src/http/server.rs | 2 +- .../nym-node-status-api/src/logging.rs | 2 +- .../nym-node-status-api/src/monitor/mod.rs | 2 +- .../nym-node-status-api/src/testruns/queue.rs | 5 +- .../nym-node-status-client/src/lib.rs | 2 +- nym-node/src/logging.rs | 2 +- .../src/throughput_tester/global_stats.rs | 4 +- nym-statistics-api/src/http/server.rs | 2 +- nym-statistics-api/src/logging.rs | 2 +- nym-wallet/Cargo.lock | 1 + nym-wallet/src-tauri/src/config/mod.rs | 16 ++-- nym-wallet/src-tauri/src/network_config.rs | 4 +- .../src-tauri/src/operations/app/link.rs | 4 +- .../src-tauri/src/operations/app/version.rs | 9 +- .../src-tauri/src/operations/app/window.rs | 2 +- .../src/operations/mixnet/account.rs | 4 +- .../src-tauri/src/operations/mixnet/bond.rs | 64 ++++++-------- .../src/operations/mixnet/delegate.rs | 83 ++++++------------- .../src/operations/mixnet/interval.rs | 2 +- .../src/operations/mixnet/rewards.rs | 10 +-- .../src-tauri/src/operations/mixnet/send.rs | 9 +- .../src/operations/signatures/sign.rs | 10 +-- .../src/operations/simulate/mixnet.rs | 10 +-- .../src/operations/simulate/vesting.rs | 6 +- .../src-tauri/src/operations/vesting/bond.rs | 55 ++++-------- .../src/operations/vesting/delegate.rs | 14 +--- .../src/operations/vesting/migrate.rs | 6 +- .../src/operations/vesting/queries.rs | 14 ++-- .../src/operations/vesting/rewards.rs | 9 +- .../src-tauri/src/wallet_storage/mod.rs | 15 ++-- nyx-chain-watcher/build.rs | 6 +- nyx-chain-watcher/src/cli/commands/run/mod.rs | 2 +- nyx-chain-watcher/src/http/server.rs | 2 +- nyx-chain-watcher/src/logging.rs | 2 +- sdk/rust/nym-sdk/Cargo.toml | 11 +-- sdk/rust/nym-sdk/examples/surb_reply.rs | 3 +- .../examples/tcp_proxy_single_connection.rs | 4 +- .../src/client_pool/mixnet_client_pool.rs | 2 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 2 +- sdk/rust/nym-sdk/src/mixnet/native_client.rs | 8 +- .../authenticator/src/cli/peer_handler.rs | 10 +-- .../authenticator/src/cli/run.rs | 2 +- .../authenticator/src/mixnet_listener.rs | 4 +- .../ip-packet-router/src/cli/run.rs | 2 +- .../src/clients/connected_client_handler.rs | 4 +- .../src/messages/response/v8.rs | 3 +- .../ip-packet-router/src/mixnet_listener.rs | 4 +- .../network-requester/examples/query.rs | 2 +- .../network-requester/src/cli/run.rs | 2 +- tools/echo-server/src/lib.rs | 4 +- tools/internal/testnet-manager/build.rs | 4 +- tools/nym-cli/src/main.rs | 5 +- tools/nym-nr-query/src/main.rs | 8 +- wasm/client/src/encoded_payload_helper.rs | 4 +- 168 files changed, 393 insertions(+), 523 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1c0b12587..e20ac11d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6135,6 +6135,7 @@ dependencies = [ "futures", "mime", "serde", + "serde_json", "serde_yaml", "subtle 2.6.1", "time", @@ -6998,6 +6999,7 @@ dependencies = [ "tap", "tempfile", "thiserror 2.0.12", + "time", "tokio", "tokio-stream", "tokio-util", diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 2df449385d..761e2b9531 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -318,7 +318,7 @@ impl Handler { async fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> { debug!("Handling text message request"); - trace!("Content: {:?}", msg); + trace!("Content: {msg:?}"); self.received_response_type = ReceivedResponseType::Text; let client_request = ClientRequest::try_from_text(msg); diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index cc36198d40..a1b430a930 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -68,9 +68,9 @@ impl Listener { new_conn = tcp_listener.accept() => { match new_conn { Ok((mut socket, remote_addr)) => { - debug!("Received connection from {:?}", remote_addr); + debug!("Received connection from {remote_addr:?}"); if self.state.is_connected() { - warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr); + warn!("Tried to open a duplicate websocket connection. The request came from {remote_addr}"); // if we've already got a connection, don't allow another one // while we only ever want to accept a single connection, we don't want // to leave clients hanging (and also allow for reconnection if it somehow diff --git a/common/async-file-watcher/src/lib.rs b/common/async-file-watcher/src/lib.rs index 62bb895bcd..0fdeb7da3a 100644 --- a/common/async-file-watcher/src/lib.rs +++ b/common/async-file-watcher/src/lib.rs @@ -137,7 +137,7 @@ impl AsyncFileWatcher { log::error!("the file watcher receiver has been dropped!"); } } else { - log::debug!("will not propagate information about {:?}", event); + log::debug!("will not propagate information about {event:?}"); } } Err(err) => { diff --git a/common/bandwidth-controller/src/event.rs b/common/bandwidth-controller/src/event.rs index ee968dfd93..e5f78228dd 100644 --- a/common/bandwidth-controller/src/event.rs +++ b/common/bandwidth-controller/src/event.rs @@ -11,7 +11,7 @@ impl std::fmt::Display for BandwidthStatusMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BandwidthStatusMessage::RemainingBandwidth(b) => { - write!(f, "remaining bandwidth: {}", b) + write!(f, "remaining bandwidth: {b}") } BandwidthStatusMessage::NoBandwidth => write!(f, "no bandwidth left"), } diff --git a/common/client-core/src/client/mix_traffic/mod.rs b/common/client-core/src/client/mix_traffic/mod.rs index 1114de9d89..0c277c1588 100644 --- a/common/client-core/src/client/mix_traffic/mod.rs +++ b/common/client-core/src/client/mix_traffic/mod.rs @@ -146,7 +146,7 @@ impl MixTrafficController { Some(client_request) => { match self.gateway_transceiver.send_client_request(client_request).await { Ok(_) => (), - Err(e) => error!("Failed to send client request: {}", e), + Err(e) => error!("Failed to send client request: {e}"), }; }, None => { diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 4b5d483ee5..10b129d817 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -65,7 +65,7 @@ impl AcknowledgementListener { return; } - trace!("Received {} from the mix network", frag_id); + trace!("Received {frag_id} from the mix network"); self.stats_tx .report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into()); if let Err(err) = self diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 6235b7c477..4bdb7afcda 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -126,7 +126,7 @@ impl ActionController { fn handle_insert(&mut self, pending_acks: Vec<PendingAcknowledgement>) { for pending_ack in pending_acks { let frag_id = pending_ack.message_chunk.fragment_identifier(); - trace!("{} is inserted", frag_id); + trace!("{frag_id} is inserted"); if self .pending_acks_data @@ -161,22 +161,16 @@ impl ActionController { let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); *queue_key = Some(new_queue_key) } else { - debug!( - "Tried to START TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to START TIMER on pending ack that is already gone! - {frag_id}"); } } fn handle_remove(&mut self, frag_id: FragmentIdentifier) { - trace!("{} is getting removed", frag_id); + trace!("{frag_id} is getting removed"); match self.pending_acks_data.remove(&frag_id) { None => { - debug!( - "Tried to REMOVE pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack that is already gone! - {frag_id}"); } Some((_, queue_key)) => { if let Some(queue_key) = queue_key { @@ -188,10 +182,7 @@ impl ActionController { } else { // I'm not 100% sure if having a `None` key is even possible here // (REMOVE would have to be called before START TIMER), - debug!( - "Tried to REMOVE pending ack without TIMER active - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack without TIMER active - {frag_id}"); } } } @@ -200,7 +191,7 @@ impl ActionController { // initiated basically as a first step of retransmission. At first data has its delay updated // (as new sphinx packet was created with new expected delivery time) fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { - trace!("{} is updating its delay", frag_id); + trace!("{frag_id} is updating its delay"); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) @@ -213,10 +204,7 @@ impl ActionController { self.pending_acks_data .insert(frag_id, (Arc::new(inner_data), queue_key)); } else { - debug!( - "Tried to UPDATE TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to UPDATE TIMER on pending ack that is already gone! - {frag_id}"); } } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index edc313990f..0e36ce2571 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -202,7 +202,7 @@ where // well technically the message was not sent just yet, but now it's up to internal // queues and client load rather than the required delay. So realistically we can treat // whatever is about to happen as negligible additional delay. - trace!("{} is about to get sent to the mixnet", frag_id); + trace!("{frag_id} is about to get sent to the mixnet"); if let Err(err) = self.sent_notifier.unbounded_send(frag_id) { error!("Failed to notify about sent message: {err}"); } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index d038ac624e..234fdf91fb 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -164,11 +164,11 @@ impl SendingDelayController { self.current_multiplier() ); if self.current_multiplier() > 0 { - log::debug!("{}", status_str); + log::debug!("{status_str}"); } else if self.current_multiplier() > 1 { - log::info!("{}", status_str); + log::info!("{status_str}"); } else if self.current_multiplier() > 2 { - log::warn!("{}", status_str); + log::warn!("{status_str}"); } self.time_when_logged_about_elevated_multiplier = now; } diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index 380e519460..7504a4d39a 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -221,10 +221,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> { let stored_messages = std::mem::take(&mut guard.messages); if !stored_messages.is_empty() { if let Err(err) = sender.unbounded_send(stored_messages) { - error!( - "The sender channel we just received is already invalidated - {:?}", - err - ); + error!("The sender channel we just received is already invalidated - {err:?}"); // put the values back to the buffer // the returned error has two fields: err: SendError and val: T, // where val is the value that was failed to get sent; diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index c043af1bdf..bd649f67e9 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -217,14 +217,14 @@ where .surbs_storage_ref() .contains_surbs_for(&recipient_tag) { - warn!("received reply request for {:?} but we don't have any surbs stored for that recipient!", recipient_tag); + warn!("received reply request for {recipient_tag:?} but we don't have any surbs stored for that recipient!"); return; } - trace!("handling reply to {:?}", recipient_tag); + trace!("handling reply to {recipient_tag:?}"); let mut fragments = self.message_handler.split_reply_message(data); let total_size = fragments.len(); - trace!("This reply requires {:?} SURBs", total_size); + trace!("This reply requires {total_size:?} SURBs"); let available_surbs = self .full_reply_storage @@ -327,10 +327,7 @@ where .await { let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); - warn!( - "failed to request additional surbs from {:?} - {err}", - target - ); + warn!("failed to request additional surbs from {target:?} - {err}"); return Err(err); } else { self.full_reply_storage @@ -409,10 +406,7 @@ where err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); self.re_insert_pending_retransmission(&target, to_take); - warn!( - "failed to clear pending retransmission queue for {:?} - {err}", - target - ); + warn!("failed to clear pending retransmission queue for {target:?} - {err}"); return; } }; @@ -489,7 +483,7 @@ where let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); self.re_insert_pending_replies(&target, to_send); - warn!("failed to clear pending queue for {:?} - {err}", target); + warn!("failed to clear pending queue for {target:?} - {err}"); } } else { trace!("the pending queue is empty"); @@ -816,7 +810,7 @@ where if diff > max_drop_wait { to_remove.push(*pending_reply_target) } else { - debug!("We haven't received any surbs in {:?} from {pending_reply_target}. Going to explicitly ask for more", diff); + debug!("We haven't received any surbs in {diff:?} from {pending_reply_target}. Going to explicitly ask for more"); to_request.push(*pending_reply_target); } } diff --git a/common/client-core/src/client/statistics_control.rs b/common/client-core/src/client/statistics_control.rs index 01f4d25f1b..6a9b1f0b09 100644 --- a/common/client-core/src/client/statistics_control.rs +++ b/common/client-core/src/client/statistics_control.rs @@ -93,7 +93,7 @@ impl StatisticsControl { None, ); if let Err(err) = self.report_tx.send(report_message).await { - log::error!("Failed to report client stats: {:?}", err); + log::error!("Failed to report client stats: {err:?}"); } else { self.stats.reset(); } diff --git a/common/client-core/src/client/transmission_buffer.rs b/common/client-core/src/client/transmission_buffer.rs index e6eb7d6891..d110766c35 100644 --- a/common/client-core/src/client/transmission_buffer.rs +++ b/common/client-core/src/client/transmission_buffer.rs @@ -211,7 +211,7 @@ impl<T> TransmissionBuffer<T> { }; let msg = self.pop_front_from_lane(&lane)?; - log::trace!("picking to send from lane: {:?}", lane); + log::trace!("picking to send from lane: {lane:?}"); Some((lane, msg)) } diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 748e39dbb1..ada2411732 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -110,7 +110,7 @@ pub async fn gateways_for_init<R: Rng>( let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes; info!("nym api reports {} gateways", gateways.len()); - log::trace!("Gateways: {:#?}", gateways); + log::trace!("Gateways: {gateways:#?}"); // filter out gateways below minimum performance and ones that could operate as a mixnode // (we don't want instability) @@ -121,7 +121,7 @@ pub async fn gateways_for_init<R: Rng>( .filter_map(|gateway| gateway.try_into().ok()) .collect::<Vec<_>>(); log::debug!("After checking validity: {}", valid_gateways.len()); - log::trace!("Valid gateways: {:#?}", valid_gateways); + log::trace!("Valid gateways: {valid_gateways:#?}"); log::info!( "and {} after validity and performance filtering", @@ -286,7 +286,7 @@ pub(super) fn get_specified_gateway( gateways: &[RoutingNode], must_use_tls: bool, ) -> Result<RoutingNode, ClientCoreError> { - log::debug!("Requesting specified gateway: {}", gateway_identity); + log::debug!("Requesting specified gateway: {gateway_identity}"); let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 0d6e40ffbc..4223263e5a 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -49,14 +49,14 @@ pub fn show_error<E>(e: E) where E: Display, { - error!("{}", e); + error!("{e}"); } pub fn show_error_passthrough<E>(e: E) -> E where E: Error + Display, { - error!("{}", e); + error!("{e}"); e } diff --git a/common/commands/src/validator/account/balance.rs b/common/commands/src/validator/account/balance.rs index 4e109ad7b3..26ebd86a52 100644 --- a/common/commands/src/validator/account/balance.rs +++ b/common/commands/src/validator/account/balance.rs @@ -42,7 +42,7 @@ pub async fn query_balance( .address .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); - info!("Getting balance for {}...", address); + info!("Getting balance for {address}..."); match client.get_all_balances(&address).await { Ok(coins) => { diff --git a/common/commands/src/validator/account/pubkey.rs b/common/commands/src/validator/account/pubkey.rs index 6a7c3383d6..ed419c1101 100644 --- a/common/commands/src/validator/account/pubkey.rs +++ b/common/commands/src/validator/account/pubkey.rs @@ -57,17 +57,17 @@ pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip3 println!("{}", account.public_key().to_string()); } None => { - error!("Could not derive key that matches {}", address) + error!("Could not derive key that matches {address}") } }, Err(e) => { - error!("Failed to derive accounts. {}", e); + error!("Failed to derive accounts. {e}"); } } } pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) { - info!("Getting public key for address {} from chain...", address); + info!("Getting public key for address {address} from chain..."); match client.get_account(&address).await { Ok(Some(account)) => { if let Ok(base_account) = account.try_get_base_account() { diff --git a/common/commands/src/validator/account/send_multiple.rs b/common/commands/src/validator/account/send_multiple.rs index 585aa896f3..762efdb8ab 100644 --- a/common/commands/src/validator/account/send_multiple.rs +++ b/common/commands/src/validator/account/send_multiple.rs @@ -37,7 +37,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { let rows = InputFileReader::new(&args.input); if let Err(e) = rows { - error!("Failed to read input file: {}", e); + error!("Failed to read input file: {e}"); return; } let rows = rows.unwrap(); @@ -67,7 +67,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { .prompt(); if let Err(e) = ans { - info!("Aborting, {}...", e); + info!("Aborting, {e}..."); return; } if let Ok(false) = ans { @@ -100,13 +100,10 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { println!("Transaction hash: {}", &res.hash); if let Some(output_filename) = args.output { - println!("\nWriting output log to {}", output_filename); + println!("\nWriting output log to {output_filename}"); if let Err(e) = write_output_file(rows, res, &output_filename) { - error!( - "Failed to write output file {} with error {}", - output_filename, e - ); + error!("Failed to write output file {output_filename} with error {e}"); } } } @@ -136,7 +133,7 @@ fn write_output_file( .collect::<Vec<String>>() .join("\n"); - Ok(file.write_all(format!("{}\n", data).as_bytes())?) + Ok(file.write_all(format!("{data}\n").as_bytes())?) } #[derive(Debug)] @@ -171,7 +168,7 @@ impl InputFileReader { // multiply when a whole token amount, e.g. 50nym (50.123456nym is not allowed, that must be input as 50123456unym) let (amount, denom) = if !denom.starts_with('u') { - (amount * 1_000_000u128, format!("u{}", denom)) + (amount * 1_000_000u128, format!("u{denom}")) } else { (amount, denom) }; diff --git a/common/commands/src/validator/cosmwasm/execute_contract.rs b/common/commands/src/validator/cosmwasm/execute_contract.rs index 0dfc547430..e5faa5cd03 100644 --- a/common/commands/src/validator/cosmwasm/execute_contract.rs +++ b/common/commands/src/validator/cosmwasm/execute_contract.rs @@ -55,6 +55,6 @@ pub async fn execute(args: Args, client: SigningClient) { .await { Ok(res) => info!("SUCCESS ✅\n{}", json!(res)), - Err(e) => error!("FAILURE ❌\n{}", e), + Err(e) => error!("FAILURE ❌\n{e}"), } } diff --git a/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs b/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs index 3a80ff4ca5..5f1b57b019 100644 --- a/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs +++ b/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs @@ -43,7 +43,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let multisig_addr = args.multisig_addr.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) @@ -97,7 +97,7 @@ pub async fn generate(args: Args) { key_size: DEFAULT_DEALINGS as u32, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs index f13bb65151..52244d761b 100644 --- a/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs +++ b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs @@ -28,7 +28,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let group_addr = args.group_addr.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS) @@ -51,7 +51,7 @@ pub async fn generate(args: Args) { deposit_amount: args.deposit_amount, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index 8a8081400e..9226a02653 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -88,7 +88,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate mixnet contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let initial_rewarding_params = InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(args.initial_reward_pool, 0) @@ -114,7 +114,7 @@ pub async fn generate(args: Args) { }, }; - debug!("initial_rewarding_params: {:?}", initial_rewarding_params); + debug!("initial_rewarding_params: {initial_rewarding_params:?}"); let rewarding_validator_address = args.rewarding_validator_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) @@ -160,7 +160,7 @@ pub async fn generate(args: Args) { key_validity_in_epochs: None, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/multisig.rs b/common/commands/src/validator/cosmwasm/generators/multisig.rs index 90abae9e28..8b0b79e4fe 100644 --- a/common/commands/src/validator/cosmwasm/generators/multisig.rs +++ b/common/commands/src/validator/cosmwasm/generators/multisig.rs @@ -31,7 +31,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS) @@ -60,7 +60,7 @@ pub async fn generate(args: Args) { coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(), }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/vesting.rs b/common/commands/src/validator/cosmwasm/generators/vesting.rs index 94f5cda0b2..536520fa9c 100644 --- a/common/commands/src/validator/cosmwasm/generators/vesting.rs +++ b/common/commands/src/validator/cosmwasm/generators/vesting.rs @@ -21,7 +21,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let mixnet_contract_address = args.mixnet_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::MIXNET_CONTRACT_ADDRESS) @@ -39,7 +39,7 @@ pub async fn generate(args: Args) { mix_denom, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/init_contract.rs b/common/commands/src/validator/cosmwasm/init_contract.rs index 430440be44..ca239d8f1a 100644 --- a/common/commands/src/validator/cosmwasm/init_contract.rs +++ b/common/commands/src/validator/cosmwasm/init_contract.rs @@ -72,7 +72,7 @@ pub async fn init(args: Args, client: SigningClient, network_details: &NymNetwor .await .expect("failed to instantiate the contract!"); - info!("Init result: {:?}", res); + info!("Init result: {res:?}"); println!("{}", res.contract_address) } diff --git a/common/commands/src/validator/cosmwasm/migrate_contract.rs b/common/commands/src/validator/cosmwasm/migrate_contract.rs index f106ae8b17..2dd38ccdd8 100644 --- a/common/commands/src/validator/cosmwasm/migrate_contract.rs +++ b/common/commands/src/validator/cosmwasm/migrate_contract.rs @@ -47,5 +47,5 @@ pub async fn migrate(args: Args, client: SigningClient) { .expect("failed to migrate the contract!") }; - info!("Migrate result: {:?}", res); + info!("Migrate result: {res:?}"); } diff --git a/common/commands/src/validator/cosmwasm/upload_contract.rs b/common/commands/src/validator/cosmwasm/upload_contract.rs index 96d01741ee..5a17b66013 100644 --- a/common/commands/src/validator/cosmwasm/upload_contract.rs +++ b/common/commands/src/validator/cosmwasm/upload_contract.rs @@ -31,7 +31,7 @@ pub async fn upload(args: Args, client: SigningClient) { .await .expect("failed to upload the contract!"); - info!("Upload result: {:?}", res); + info!("Upload result: {res:?}"); println!("{}", res.code_id) } diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index dc8a2d591f..f3b99513f7 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -47,5 +47,5 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { .await .expect("failed to delegate to mixnode!"); - info!("delegating to mixnode: {:?}", res); + info!("delegating to mixnode: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs index 96bf95be04..70eb85e7cd 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs @@ -196,7 +196,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { let records = match InputFileReader::new(&args.input) { Ok(records) => records, Err(e) => { - println!("Error reading input file: {}", e); + println!("Error reading input file: {e}"); return; } }; @@ -262,11 +262,11 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { } if !undelegation_msgs.is_empty() { - println!("Undelegation records : \n{}\n\n", undelegation_table); + println!("Undelegation records : \n{undelegation_table}\n\n"); } if !delegation_msgs.is_empty() { - println!("Delegation records : \n{}\n\n", delegation_table); + println!("Delegation records : \n{delegation_table}\n\n"); } let ans = inquire::Confirm::new("Do you want to continue with the shown operations?") @@ -275,7 +275,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { .prompt(); if let Err(e) = ans { - info!("Aborting, {}...", e); + info!("Aborting, {e}..."); return; } @@ -348,7 +348,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { if args.output.is_some() { if let Err(e) = write_to_csv(output_details, args.output) { - info!("Failed to write to CSV, {}", e); + info!("Failed to write to CSV, {e}"); } } } diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs index 8ac1ab9c07..66a2a5d659 100644 --- a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -38,5 +38,5 @@ pub async fn migrate_vested_delegation(args: Args, client: SigningClient) { .await .expect("failed to migrate delegation!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 83426a316e..8b876831af 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -40,5 +40,5 @@ pub async fn claim_delegator_reward(args: Args, client: SigningClient) { .await .expect("failed to claim delegator-reward"); - info!("Claiming delegator reward: {:?}", res) + info!("Claiming delegator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index d36c4a7b17..b0e9df21bd 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -40,5 +40,5 @@ pub async fn vesting_claim_delegator_reward(args: Args, client: SigningClient) { .await .expect("failed to claim vesting delegator-reward"); - info!("Claiming vesting delegator reward: {:?}", res) + info!("Claiming vesting delegator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index 19593ed5ce..edcd646e90 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -40,5 +40,5 @@ pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { .await .expect("failed to remove stake from mixnode!"); - info!("removing stake from mixnode: {:?}", res) + info!("removing stake from mixnode: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 3fa3fd7cee..28351541f5 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -53,5 +53,5 @@ pub async fn vesting_delegate_to_mixnode(args: Args, client: SigningClient) { .await .expect("failed to delegate to mixnode!"); - info!("vesting delegating to mixnode: {:?}", res); + info!("vesting delegating to mixnode: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 9daf8691d3..c02f87e055 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -45,5 +45,5 @@ pub async fn vesting_undelegate_from_mixnode(args: Args, client: SigningClient) .await .expect("failed to remove stake from vesting account on mixnode!"); - info!("removing stake from vesting mixnode: {:?}", res) + info!("removing stake from vesting mixnode: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs index 28fe160070..c11246c46e 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs @@ -73,5 +73,5 @@ pub async fn bond_gateway(args: Args, client: SigningClient) { .await .expect("failed to bond gateway!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs index a6b22a2d4b..c1189f1885 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs @@ -52,5 +52,5 @@ pub async fn migrate_to_nymnode(args: Args, client: SigningClient) { .await .expect("failed to migrate gateway!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs index c0cf879147..a1b5a8d4bd 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs @@ -56,5 +56,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating gateway config"); - info!("gateway config updated: {:?}", res) + info!("gateway config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs index 346b652b2f..8ebc9279bf 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs @@ -57,5 +57,5 @@ pub async fn vesting_update_config(args: Args, client: SigningClient) { .await .expect("updating vesting gateway config"); - info!("gateway config updated: {:?}", res) + info!("gateway config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs index af9c6c8bb4..cef6dbcf7f 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs @@ -17,5 +17,5 @@ pub async fn unbond_gateway(client: SigningClient) { .await .expect("failed to unbond gateway!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs index 91c2817be8..4f5f63d16b 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -73,5 +73,5 @@ pub async fn vesting_bond_gateway(args: Args, client: SigningClient) { .await .expect("failed to bond gateway!"); - info!("Vesting bonding gateway result: {:?}", res) + info!("Vesting bonding gateway result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs index 1c4312424c..72b9357dab 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs @@ -17,5 +17,5 @@ pub async fn vesting_unbond_gateway(client: SigningClient) { .await .expect("failed to unbond vesting gateway!"); - info!("Unbonding vesting result: {:?}", res) + info!("Unbonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 410ce91d28..b8de001f2d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -106,5 +106,5 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { .await .expect("failed to bond mixnode!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs index 556021dcf9..bd96aeb270 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to decrease pledge!"); - info!("decreasing pledge: {:?}", res); + info!("decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs index 95bd9d0573..ae2ec7395d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs @@ -15,5 +15,5 @@ pub async fn migrate_vested_mixnode(_args: Args, client: SigningClient) { .await .expect("failed to migrate mixnode!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs index fe46e2c11a..3f736c86f4 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs @@ -15,5 +15,5 @@ pub async fn migrate_to_nymnode(_args: Args, client: SigningClient) { .await .expect("failed to migrate mixnode!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs b/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs index 1989725907..51ba9daa4d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs @@ -25,5 +25,5 @@ pub async fn pledge_more(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("pledging more: {:?}", res); + info!("pledging more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs index a5f8d65236..f3e97d0df2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs @@ -17,5 +17,5 @@ pub async fn claim_operator_reward(_args: Args, client: SigningClient) { .await .expect("failed to claim operator reward"); - info!("Claiming operator reward: {:?}", res) + info!("Claiming operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs index 3b88c6cf5c..38c2ccf41c 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs @@ -20,5 +20,5 @@ pub async fn vesting_claim_operator_reward(client: SigningClient) { .await .expect("failed to claim vesting operator reward"); - info!("Claiming vesting operator reward: {:?}", res) + info!("Claiming vesting operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs index 733dea04a1..31db3435b2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs @@ -64,5 +64,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating mix-node config"); - info!("mixnode config updated: {:?}", res) + info!("mixnode config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs index a495c8f4b5..092aa54d82 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs @@ -65,5 +65,5 @@ pub async fn vesting_update_config(client: SigningClient, args: Args) { .await .expect("updating vesting mix-node config"); - info!("mixnode config updated: {:?}", res) + info!("mixnode config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs index fe6f3df223..bf6864ef5e 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs @@ -18,5 +18,5 @@ pub async fn unbond_mixnode(_args: Args, client: SigningClient) { .await .expect("failed to unbond mixnode!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index d55d6463e7..6813ea2dfd 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -106,5 +106,5 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str .await .expect("failed to bond vesting mixnode!"); - info!("Bonding vesting result: {:?}", res) + info!("Bonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs index bb83e1ffaa..1ffb1de360 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn vesting_decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to vesting decrease pledge!"); - info!("vesting decreasing pledge: {:?}", res); + info!("vesting decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs index 9917e02860..e53c9b9cfe 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs @@ -26,5 +26,5 @@ pub async fn vesting_pledge_more(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("vesting pledge more: {:?}", res); + info!("vesting pledge more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs index 3e63846430..b3d52bdb73 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs @@ -20,5 +20,5 @@ pub async fn vesting_unbond_mixnode(client: SigningClient) { .await .expect("failed to unbond vesting mixnode!"); - info!("Unbonding vesting result: {:?}", res) + info!("Unbonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs index acde3d39e3..145e245863 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs @@ -85,5 +85,5 @@ pub async fn bond_nymnode(args: Args, client: SigningClient) { .await .expect("failed to bond nymnode!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs index a299ddbe65..4bb20b4a66 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to decrease pledge!"); - info!("decreasing pledge: {:?}", res); + info!("decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs index 09b94bc5d5..31a0eb7a8d 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs @@ -25,5 +25,5 @@ pub async fn increase_pledge(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("pledging more: {:?}", res); + info!("pledging more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs index a8f157f661..005e81f069 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs @@ -17,5 +17,5 @@ pub async fn claim_operator_reward(_args: Args, client: SigningClient) { .await .expect("failed to claim operator reward"); - info!("Claiming operator reward: {:?}", res) + info!("Claiming operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs index 5ba5bf52fa..fb59924c9e 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs @@ -46,5 +46,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating nym node config"); - info!("nym node config updated: {:?}", res) + info!("nym node config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs index 5668c92e0d..a513067a02 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -68,6 +68,6 @@ pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Re .await .expect("failed to update cost params"); - info!("Cost params result: {:?}", res); + info!("Cost params result: {res:?}"); Ok(()) } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs index fbcef6bfd7..d60266a12d 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs @@ -18,5 +18,5 @@ pub async fn unbond_nymnode(_args: Args, client: SigningClient) { .await .expect("failed to unbond Nym Node!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index 424dfd184b..7df1d12eb6 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -52,7 +52,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) { println!("{}", json!(output)); } Err(e) => { - error!("Failed to sign message. {}", e); + error!("Failed to sign message. {e}"); } } } @@ -61,7 +61,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) { } }, Err(e) => { - error!("Failed to derive accounts. {}", e); + error!("Failed to derive accounts. {e}"); } } } diff --git a/common/commands/src/validator/signature/verify.rs b/common/commands/src/validator/signature/verify.rs index 391c5105d8..7168b75b92 100644 --- a/common/commands/src/validator/signature/verify.rs +++ b/common/commands/src/validator/signature/verify.rs @@ -38,7 +38,7 @@ pub async fn verify(args: Args, client: &QueryClient) { let public_key = match AccountId::from_str(&args.public_key_or_address) { Ok(address) => { - info!("Found account address instead of public key, so looking up public key for {} from chain", address); + info!("Found account address instead of public key, so looking up public key for {address} from chain"); match client.get_account_public_key(&address).await.ok() { Some(public_key) => { if let Some(k) = public_key { @@ -48,8 +48,7 @@ pub async fn verify(args: Args, client: &QueryClient) { } None => { error!( - "Address {} does not have a public key recorded on the chain. This is probably because the account has never signed a transaction.", - address + "Address {address} does not have a public key recorded on the chain. This is probably because the account has never signed a transaction." ); None } @@ -58,7 +57,7 @@ pub async fn verify(args: Args, client: &QueryClient) { Err(_) => match PublicKey::from_json(&args.public_key_or_address) { Ok(parsed) => Some(parsed), Err(e) => { - error!("Public key should be JSON. Unable to parse: {}", e); + error!("Public key should be JSON. Unable to parse: {e}"); None } }, @@ -78,7 +77,7 @@ pub async fn verify(args: Args, client: &QueryClient) { ) { Ok(()) => println!("SUCCESS ✅ signature verified"), Err(e) => { - error!("FAILURE ❌ Signature verification failed: {}", e); + error!("FAILURE ❌ Signature verification failed: {e}"); } } } diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 35ee669dab..b166202af0 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -86,6 +86,6 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw .await .unwrap(); - info!("Vesting result: {:?}", res); - info!("Coin send result: {:?}", send_coin_response); + info!("Vesting result: {res:?}"); + info!("Coin send result: {send_coin_response:?}"); } diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs index 0baca858f1..df3736b94d 100644 --- a/common/commands/src/validator/vesting/query_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -29,7 +29,7 @@ pub async fn query(args: Args, client: QueryClient, address_from_mnemonic: Optio .address .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); - info!("Checking account {} for a vesting schedule...", account_id); + info!("Checking account {account_id} for a vesting schedule..."); let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index be7133b4c2..1aeab19ed8 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -95,7 +95,7 @@ where } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { // Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries let secs_until_final = final_timestamp.saturating_sub(current_timestamp_secs) + 1; - info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final); + info!("Approximately {secs_until_final} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment."); tokio::time::sleep(Duration::from_secs(secs_until_final)).await; } else if matches!(epoch.state, EpochState::WaitingInitialisation) { info!("dkg hasn't been initialised yet and it is not known when it will be. Going to check again later"); diff --git a/common/gateway-stats-storage/build.rs b/common/gateway-stats-storage/build.rs index 166349c67a..cef40420c9 100644 --- a/common/gateway-stats-storage/build.rs +++ b/common/gateway-stats-storage/build.rs @@ -7,9 +7,9 @@ use std::env; #[tokio::main] async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/gateway-stats-example.sqlite", out_dir); + let database_path = format!("{out_dir}/gateway-stats-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await .expect("Failed to create SQLx database connection"); diff --git a/common/gateway-storage/build.rs b/common/gateway-storage/build.rs index 27d55fccd2..9b46e07840 100644 --- a/common/gateway-storage/build.rs +++ b/common/gateway-storage/build.rs @@ -7,9 +7,9 @@ use std::env; #[tokio::main] async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/gateway-example.sqlite", out_dir); + let database_path = format!("{out_dir}/gateway-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await .expect("Failed to create SQLx database connection"); diff --git a/common/gateway-storage/src/clients.rs b/common/gateway-storage/src/clients.rs index 96b5f857eb..8bee61e04b 100644 --- a/common/gateway-storage/src/clients.rs +++ b/common/gateway-storage/src/clients.rs @@ -37,7 +37,7 @@ impl std::fmt::Display for ClientType { ClientType::EntryWireguard => "entry_wireguard", ClientType::ExitWireguard => "exit_wireguard", }; - write!(f, "{}", s) + write!(f, "{s}") } } diff --git a/common/http-api-client/src/user_agent.rs b/common/http-api-client/src/user_agent.rs index fee71752bb..54fe6ac24e 100644 --- a/common/http-api-client/src/user_agent.rs +++ b/common/http-api-client/src/user_agent.rs @@ -141,7 +141,7 @@ mod tests { }; assert_eq!( - format!("{}", user_agent), + format!("{user_agent}"), "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg" ); } diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index 1de2a0a8c4..4a405c155e 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -19,6 +19,7 @@ colored = { workspace = true, optional = true } futures = { workspace = true, optional = true } mime = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } serde_yaml = { workspace = true, optional = true } subtle = { workspace = true, optional = true } time = { workspace = true, optional = true, features = ["macros"] } diff --git a/common/http-api-common/src/response/json.rs b/common/http-api-common/src/response/json.rs index b2c904b7ee..a1b46e7d50 100644 --- a/common/http-api-common/src/response/json.rs +++ b/common/http-api-common/src/response/json.rs @@ -7,7 +7,6 @@ use axum::http::{header, HeaderValue}; use axum::response::{IntoResponse, Response}; use bytes::{BufMut, BytesMut}; use serde::Serialize; -use utoipa::gen::serde_json; // don't use axum's Json directly as we need to be able to define custom headers #[derive(Debug, Clone, Default)] diff --git a/common/ip-packet-requests/src/v8/request.rs b/common/ip-packet-requests/src/v8/request.rs index c5e49e83b7..964065578b 100644 --- a/common/ip-packet-requests/src/v8/request.rs +++ b/common/ip-packet-requests/src/v8/request.rs @@ -191,7 +191,7 @@ impl fmt::Display for IpPacketRequestData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { IpPacketRequestData::Data(_) => write!(f, "Data"), - IpPacketRequestData::Control(c) => write!(f, "Control({})", c), + IpPacketRequestData::Control(c) => write!(f, "Control({c})"), } } } diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs index 64de45df77..2164bd2820 100644 --- a/common/network-defaults/build.rs +++ b/common/network-defaults/build.rs @@ -30,7 +30,7 @@ fn main() { for var in variables_to_track { // if script fails, debug with `cargo check -vv`` - println!("Looking for {}", var); + println!("Looking for {var}"); // read pattern that looks like: // <var>: &str = "<whatever is between quotes>" @@ -41,7 +41,7 @@ fn main() { .captures(source_of_truth) .and_then(|caps| caps.get(1).map(|match_| match_.as_str().to_string())) .expect("Couldn't find var in source file"); - println!("Storing {}={}", var, value); + println!("Storing {var}={value}"); replace_with.insert(var, value); } @@ -57,13 +57,11 @@ fn main() { // <var>=<value> let re = Regex::new(&pattern).unwrap(); contents = re - .replace(&contents, |_: ®ex::Captures| { - format!(r#"{}={}"#, var, value) - }) + .replace(&contents, |_: ®ex::Captures| format!(r#"{var}={value}"#)) .to_string(); } - println!("File contents to write:\n{}", contents); + println!("File contents to write:\n{contents}"); if output_path.exists() { fs::write(output_path, contents).unwrap(); } else { diff --git a/common/network-defaults/src/env_setup.rs b/common/network-defaults/src/env_setup.rs index 4ab07259a7..fb67056387 100644 --- a/common/network-defaults/src/env_setup.rs +++ b/common/network-defaults/src/env_setup.rs @@ -25,7 +25,7 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) { .expect("Invalid path to environment configuration file"); for item in items { let (key, val) = item.expect("Invalid item in environment configuration file"); - log::debug!("{}: {}", key, val); + log::debug!("{key}: {val}"); } } diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 28cf7e2349..e8fd8fcfa8 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -119,7 +119,7 @@ impl NymNetworkDetails { } } Err(VarError::NotPresent) => None, - err => panic!("Unable to set: {:?}", err), + err => panic!("Unable to set: {err:?}"), } } diff --git a/common/nym-metrics/src/lib.rs b/common/nym-metrics/src/lib.rs index dbe950e48a..8694ea73d1 100644 --- a/common/nym-metrics/src/lib.rs +++ b/common/nym-metrics/src/lib.rs @@ -344,9 +344,9 @@ impl fmt::Display for MetricsController { let metrics = self.gather(); let output = match String::from_utf8(metrics) { Ok(output) => output, - Err(e) => return write!(f, "Error decoding metrics to String: {}", e), + Err(e) => return write!(f, "Error decoding metrics to String: {e}"), }; - write!(f, "{}", output) + write!(f, "{output}") } } @@ -597,7 +597,7 @@ mod tests { assert_eq!(literal, "nym_metrics_foo"); let bar = "bar"; - let format = format!("foomp_{}", bar); + let format = format!("foomp_{bar}"); let formatted = prepend_package_name!(format); assert_eq!(formatted, "nym_metrics_foomp_bar"); } diff --git a/common/nym_offline_compact_ecash/src/scheme/setup.rs b/common/nym_offline_compact_ecash/src/scheme/setup.rs index dd469fb009..349706fe47 100644 --- a/common/nym_offline_compact_ecash/src/scheme/setup.rs +++ b/common/nym_offline_compact_ecash/src/scheme/setup.rs @@ -26,7 +26,7 @@ impl GroupParameters { pub fn new(attributes: usize) -> GroupParameters { assert!(attributes > 0); let gammas = (1..=attributes) - .map(|i| hash_g1(format!("gamma{}", i))) + .map(|i| hash_g1(format!("gamma{i}"))) .collect(); let delta = hash_g1("delta"); diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index 9c112e7928..17140d39fc 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -197,7 +197,7 @@ where let res = tokio::select! { biased; message = receiver.next() => { - log::debug!("Received message: {:?}", message); + log::debug!("Received message: {message:?}"); match message { Some(Socks5ControlMessage::Stop) => { log::info!("Received stop message"); @@ -209,7 +209,7 @@ where Ok(()) } Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } _ = tokio::signal::ctrl_c() => { diff --git a/common/socks5-client-core/src/socks/client.rs b/common/socks5-client-core/src/socks/client.rs index 572849d296..5d79d66380 100644 --- a/common/socks5-client-core/src/socks/client.rs +++ b/common/socks5-client-core/src/socks/client.rs @@ -579,7 +579,7 @@ impl SocksClient { ); // Get valid auth methods let methods = self.get_available_methods().await?; - trace!("methods: {:?}", methods); + trace!("methods: {methods:?}"); let mut response = [0u8; 2]; diff --git a/common/socks5-client-core/src/socks/mixnet_responses.rs b/common/socks5-client-core/src/socks/mixnet_responses.rs index 05cb5bd48d..f74681c8de 100644 --- a/common/socks5-client-core/src/socks/mixnet_responses.rs +++ b/common/socks5-client-core/src/socks/mixnet_responses.rs @@ -61,7 +61,7 @@ impl MixnetResponseListener { control_response: ControlResponse, ) -> Result<(), Socks5ClientCoreError> { error!("received a control response which we don't know how to handle yet!"); - error!("got: {:?}", control_response); + error!("got: {control_response:?}"); // I guess we'd need another channel here to forward those to where they need to go @@ -88,7 +88,7 @@ impl MixnetResponseListener { } Socks5ResponseContent::Query(response) => { error!("received a query response which we don't know how to handle yet!"); - error!("got: {:?}", response); + error!("got: {response:?}"); // I guess we'd need another channel here to forward those to where they need to go diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index e63df37851..6cdf0efa9c 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -122,8 +122,7 @@ where biased; _ = &mut shutdown_future => { debug!( - "closing inbound proxy after outbound was closed {:?} ago", - SHUTDOWN_TIMEOUT + "closing inbound proxy after outbound was closed {SHUTDOWN_TIMEOUT:?} ago" ); // inform remote just in case it was closed because of lack of heartbeat. // worst case the remote will just have couple of false negatives @@ -169,7 +168,7 @@ where } } } - trace!("{} - inbound closed", connection_id); + trace!("{connection_id} - inbound closed"); shutdown_notify.notify_one(); shutdown_listener.disarm(); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 63d49fb311..dddae8d894 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -72,12 +72,12 @@ pub(super) async fn run_outbound( } } _ = &mut mix_timeout => { - warn!("didn't get anything from the client on {} mixnet in {:?}. Shutting down the proxy.", connection_id, MIX_TTL); + warn!("didn't get anything from the client on {connection_id} mixnet in {MIX_TTL:?}. Shutting down the proxy."); // If they were online it's kinda their fault they didn't send any heartbeat messages. break; } _ = &mut shutdown_future => { - debug!("closing outbound proxy after inbound was closed {:?} ago", SHUTDOWN_TIMEOUT); + debug!("closing outbound proxy after inbound was closed {SHUTDOWN_TIMEOUT:?} ago"); break; } _ = shutdown_listener.recv() => { @@ -87,7 +87,7 @@ pub(super) async fn run_outbound( } } - trace!("{} - outbound closed", connection_id); + trace!("{connection_id} - outbound closed"); shutdown_notify.notify_one(); shutdown_listener.disarm(); diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index 28a5182d00..32d9b4e9e2 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -360,7 +360,7 @@ impl Socks5RequestContent { let query_bytes: Vec<u8> = make_bincode_serializer() .serialize(&query) .tap_err(|err| { - log::error!("Failed to serialize query request: {:?}: {err}", query); + log::error!("Failed to serialize query request: {query:?}: {err}"); }) .unwrap_or_default(); std::iter::once(RequestFlag::Query as u8) diff --git a/common/socks5/requests/src/response.rs b/common/socks5/requests/src/response.rs index 584f39df5b..08e45c1de9 100644 --- a/common/socks5/requests/src/response.rs +++ b/common/socks5/requests/src/response.rs @@ -213,7 +213,7 @@ impl Socks5ResponseContent { let query_bytes: Vec<u8> = make_bincode_serializer() .serialize(&query) .tap_err(|err| { - log::error!("Failed to serialize query response: {:?}: {err}", query); + log::error!("Failed to serialize query response: {query:?}: {err}"); }) .unwrap_or_default(); std::iter::once(ResponseFlag::Query as u8) diff --git a/common/statistics/src/clients/gateway_conn_statistics.rs b/common/statistics/src/clients/gateway_conn_statistics.rs index 961c7a0f7f..bb42c676d5 100644 --- a/common/statistics/src/clients/gateway_conn_statistics.rs +++ b/common/statistics/src/clients/gateway_conn_statistics.rs @@ -77,7 +77,7 @@ impl GatewayStatsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } } diff --git a/common/statistics/src/clients/nym_api_statistics.rs b/common/statistics/src/clients/nym_api_statistics.rs index 9c3ee609c0..ddba38c0a8 100644 --- a/common/statistics/src/clients/nym_api_statistics.rs +++ b/common/statistics/src/clients/nym_api_statistics.rs @@ -77,7 +77,7 @@ impl NymApiStatsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } } diff --git a/common/statistics/src/clients/packet_statistics.rs b/common/statistics/src/clients/packet_statistics.rs index 866215c751..b2c6f56d5c 100644 --- a/common/statistics/src/clients/packet_statistics.rs +++ b/common/statistics/src/clients/packet_statistics.rs @@ -529,8 +529,8 @@ impl PacketStatisticsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } fn check_for_notable_events(&self) { diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs index 6eb1eea7ff..ad105c9f45 100644 --- a/common/statistics/src/lib.rs +++ b/common/statistics/src/lib.rs @@ -41,7 +41,7 @@ fn generate_stats_id<M: AsRef<[u8]>>(prefix: &str, id_seed: M) -> String { hasher.update(prefix); hasher.update(&id_seed); let output = hasher.finalize(); - format!("{:x}", output) + format!("{output:x}") } pub fn hash_identifier<M: AsRef<[u8]>>(identifier: M) -> String { diff --git a/common/task/src/connections.rs b/common/task/src/connections.rs index 8557b84676..35f448c622 100644 --- a/common/task/src/connections.rs +++ b/common/task/src/connections.rs @@ -94,7 +94,7 @@ impl LaneQueueLengths { log::warn!("Timeout reached while waiting for queue to clear"); break; } - log::trace!("Waiting for queue to clear ({} items left)", lane_length); + log::trace!("Waiting for queue to clear ({lane_length} items left)"); tokio::time::sleep(Duration::from_millis(100)).await; } } diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index ba605f5a55..d5af78fcc0 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -163,7 +163,7 @@ impl TaskManager { // Announce that we are operational. This means that in the application where this is used, // everything is up and running and ready to go. if let Err(msg) = sender.send(Box::new(start_status)).await { - log::error!("Error sending status message: {}", msg); + log::error!("Error sending status message: {msg}"); }; if let Some(mut task_status_rx) = self.task_status_rx.take() { diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index 483c7c630b..ebaab7b20f 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -49,7 +49,7 @@ pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), Ok(()) } Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } } @@ -63,7 +63,7 @@ pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), Ok(()) }, Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } } diff --git a/common/tun/src/linux/tun_device.rs b/common/tun/src/linux/tun_device.rs index ab3bbd71b7..936a141b3f 100644 --- a/common/tun/src/linux/tun_device.rs +++ b/common/tun/src/linux/tun_device.rs @@ -157,7 +157,7 @@ impl TunDevice { "-6", "addr", "add", - &format!("{}/{}", ipv6, netmaskv6), + &format!("{ipv6}/{netmaskv6}"), "dev", (tun.name()), ]) diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 3858d476bc..6b83c648d2 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -38,7 +38,7 @@ impl Drop for WgApiWrapper { fn drop(&mut self) { if let Err(e) = defguard_wireguard_rs::WireguardInterfaceApi::remove_interface(&self.inner) { - log::error!("Could not remove the wireguard interface: {:?}", e); + log::error!("Could not remove the wireguard interface: {e:?}"); } } } diff --git a/documentation/autodoc/src/main.rs b/documentation/autodoc/src/main.rs index 7f9513097c..812677fb99 100644 --- a/documentation/autodoc/src/main.rs +++ b/documentation/autodoc/src/main.rs @@ -163,7 +163,7 @@ fn main() -> io::Result<()> { write_output_to_file(&mut file, output)?; for (subcommand, subsubcommands) in subcommands { - writeln!(file, "\n## `{}` ", subcommand)?; + writeln!(file, "\n## `{subcommand}` ")?; let output = Command::new(main_command) .arg(subcommand) .arg("--help") @@ -195,12 +195,12 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< || get_last_word_from_filepath(main_command).unwrap() == "nym-node") && subcommand == "run" { - info!("SKIPPING {} {}", main_command, subcommand); + info!("SKIPPING {main_command} {subcommand}"); } else { let last_word = get_last_word_from_filepath(main_command); let output = Command::new(main_command).arg(subcommand).output()?; if !output.stdout.is_empty() { - info!("creating own file for {} {}", main_command, subcommand,); + info!("creating own file for {main_command} {subcommand}",); if !fs::metadata(WRITE_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -216,10 +216,7 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< write_output_to_file(&mut file, output)?; // execute help - info!( - "creating own file for {} {} --help", - main_command, subcommand, - ); + info!("creating own file for {main_command} {subcommand} --help",); if !fs::metadata(COMMAND_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -243,10 +240,7 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< debug!("empty stdout - nothing to write"); } } else { - info!( - "creating own file for {} {} --help", - main_command, subcommand, - ); + info!("creating own file for {main_command} {subcommand} --help",); if !fs::metadata(COMMAND_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -281,7 +275,7 @@ fn execute_command( if subsubcommand.is_some() { writeln!(file, "\n## `{} {}`", subcommand, subsubcommand.unwrap())?; - info!("executing {} {} --help ", main_command, subcommand); + info!("executing {main_command} {subcommand} --help "); let output = Command::new(main_command) .arg(subcommand) .arg(subsubcommand.unwrap()) @@ -294,7 +288,7 @@ fn execute_command( } // just subcommands } else { - writeln!(file, "\n## `{}`", subcommand)?; + writeln!(file, "\n## `{subcommand}`")?; // execute help let output = Command::new(main_command) @@ -318,9 +312,9 @@ fn execute_command( || get_last_word_from_filepath(main_command).unwrap() == "nymvisor" && subcommand == "run" { - info!("SKIPPING {} {}", main_command, subcommand); + info!("SKIPPING {main_command} {subcommand}"); } else { - info!("executing {} {}", main_command, subcommand); + info!("executing {main_command} {subcommand}"); let output = Command::new(main_command).arg(subcommand).output()?; if !output.stdout.is_empty() { writeln!(file, "Example output:")?; diff --git a/nym-api/build.rs b/nym-api/build.rs index 10307c5dfd..6bc476b72f 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -9,14 +9,14 @@ const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite"; #[tokio::main] async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/{}", out_dir, SQLITE_DB_FILENAME); + let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}"); #[cfg(target_family = "unix")] write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME) .await .ok(); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await .expect("Failed to create SQLx database connection"); @@ -87,8 +87,7 @@ async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Resu let mut file = File::create("enter_db.sh").await?; let contents = format!( "#!/bin/sh\n\ - sqlite3 -init settings.sql {}/{}", - out_dir, db_filename, + sqlite3 -init settings.sql {out_dir}/{db_filename}", ); file.write_all(contents.as_bytes()).await?; diff --git a/nym-api/src/ecash/dkg/state/serde_helpers.rs b/nym-api/src/ecash/dkg/state/serde_helpers.rs index 8057ef5d20..a19141e443 100644 --- a/nym-api/src/ecash/dkg/state/serde_helpers.rs +++ b/nym-api/src/ecash/dkg/state/serde_helpers.rs @@ -19,7 +19,7 @@ pub(super) mod bte_pk_serde { { let vec: Vec<u8> = Deserialize::deserialize(deserializer)?; PublicKeyWithProof::try_from_bytes(&vec) - .map_err(|err| Error::custom(format_args!("{:?}", err))) + .map_err(|err| Error::custom(format_args!("{err:?}"))) .map(Box::new) } } @@ -55,7 +55,7 @@ pub(super) mod recovered_keys { .into_iter() .map(|(idx, rec)| { RecoveredVerificationKeys::try_from_bytes(&rec) - .map_err(|err| D::Error::custom(format_args!("{:?}", err))) + .map_err(|err| D::Error::custom(format_args!("{err:?}"))) .map(|vk| (idx, vk)) }) .collect() diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index d21fd1aadb..f28021058c 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -171,9 +171,9 @@ mod tests { }; if a > b { - assert!(a - b < epsilon, "{} != {}", a, b) + assert!(a - b < epsilon, "{a} != {b}") } else { - assert!(b - a < epsilon, "{} != {}", a, b) + assert!(b - a < epsilon, "{a} != {b}") } } diff --git a/nym-api/tests/public-api/network.rs b/nym-api/tests/public-api/network.rs index 62f2440011..b6e1e3bc4c 100644 --- a/nym-api/tests/public-api/network.rs +++ b/nym-api/tests/public-api/network.rs @@ -7,7 +7,7 @@ async fn test_get_chain_status() -> Result<(), String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; let block_header = json @@ -35,7 +35,7 @@ async fn test_get_network_details() -> Result<(), String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( @@ -60,7 +60,7 @@ async fn test_get_nym_contracts() -> Result<(), String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( @@ -81,7 +81,7 @@ async fn test_get_nym_contracts_detailed() -> Result<(), String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; let mixnet_contract = json diff --git a/nym-api/tests/public-api/nym_nodes.rs b/nym-api/tests/public-api/nym_nodes.rs index 63ff057d45..dda646b3b8 100644 --- a/nym-api/tests/public-api/nym_nodes.rs +++ b/nym-api/tests/public-api/nym_nodes.rs @@ -90,7 +90,7 @@ async fn test_get_historical_performance() -> Result<(), String> { .query(&[("date", date)]) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( diff --git a/nym-api/tests/public-api/unstable_status.rs b/nym-api/tests/public-api/unstable_status.rs index fe36329646..52a0084480 100644 --- a/nym-api/tests/public-api/unstable_status.rs +++ b/nym-api/tests/public-api/unstable_status.rs @@ -87,7 +87,7 @@ async fn test_get_latest_network_monitor_run_details() -> Result<(), String> { .get(&follow_up_url) .send() .await - .map_err(|err| format!("Failed to follow up with URL {}: {}", follow_up_url, err))?; + .map_err(|err| format!("Failed to follow up with URL {follow_up_url}: {err}"))?; assert!(follow_up_res.status().is_success()); Ok(()) } diff --git a/nym-api/tests/public-api/utils.rs b/nym-api/tests/public-api/utils.rs index 96c8c3b61e..ad0f61f653 100644 --- a/nym-api/tests/public-api/utils.rs +++ b/nym-api/tests/public-api/utils.rs @@ -25,7 +25,7 @@ pub async fn make_request(url: &str) -> Result<Response, String> { .get(url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; if res.status().is_success() { Ok(res) @@ -43,7 +43,7 @@ pub async fn validate_json_response(res: Response) -> Result<Value, String> { res.json::<Value>() .await - .map_err(|err| format!("Invalid JSON response: {}", err)) + .map_err(|err| format!("Invalid JSON response: {err}")) } #[allow(dead_code)] @@ -54,11 +54,11 @@ pub async fn get_any_node_id() -> Result<String, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; let id = json .get("data") @@ -80,11 +80,11 @@ pub async fn get_mixnode_node_id() -> Result<u64, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; json.get("data") .and_then(|v| v.as_array()) @@ -110,11 +110,11 @@ pub async fn get_gateway_identity_key() -> Result<String, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; let key = json .get("data") diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 5480f96d5b..1759ccdfc3 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -389,7 +389,7 @@ mod tests { let file = NamedTempFile::new()?; let path = file.into_temp_path(); - println!("Creating database at {:?}...", path); + println!("Creating database at {path:?}..."); Ok(StorageTestWrapper { inner: VpnApiStorage::init(&path).await?, @@ -455,11 +455,11 @@ mod tests { .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); let res = res.unwrap(); - println!("res = {:?}", res); + println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Pending); println!("🚀 update_pending_blinded_share_error..."); @@ -467,11 +467,11 @@ mod tests { .update_pending_async_blinded_shares_error(0, "1234", "1234", "this is an error") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); let res = res.unwrap(); - println!("res = {:?}", res); + println!("res = {res:?}"); assert!(res.error_message.is_some()); assert_eq!(res.status, BlindedSharesStatus::Error); @@ -480,11 +480,11 @@ mod tests { .update_pending_async_blinded_shares_issued(42, "1234", "1234") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); let res = res.unwrap(); - println!("res = {:?}", res); + println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Issued); assert!(res.error_message.is_none()); diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index 9132e582c0..caddfe1db6 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -430,7 +430,7 @@ async fn db_connection(database_url: Option<&String>) -> Result<Option<(Client, let handle = tokio::spawn(async move { if let Err(e) = connection.await { - error!("Postgres connection error: {}", e); + error!("Postgres connection error: {e}"); } }); @@ -487,7 +487,7 @@ async fn submit_accounting_routes_to_db(client: Arc<Client>) -> anyhow::Result<( pub async fn submit_metrics(database_url: Option<&String>) -> anyhow::Result<()> { if let Err(e) = submit_metrics_to_db(database_url).await { - error!("Error submitting metrics to db: {}", e); + error!("Error submitting metrics to db: {e}"); } if let Some(private_key) = PRIVATE_KEY.get() { diff --git a/nym-network-monitor/src/http.rs b/nym-network-monitor/src/http.rs index 7255656344..6608345242 100644 --- a/nym-network-monitor/src/http.rs +++ b/nym-network-monitor/src/http.rs @@ -84,7 +84,7 @@ impl HttpServer { axum::serve(listener, app).with_graceful_shutdown(self.cancel.cancelled_owned()); info!("##########################################################################################"); - info!("######################### HTTP server running, with {} clients ############################################", n_clients); + info!("######################### HTTP server running, with {n_clients} clients ############################################"); info!("##########################################################################################"); server_future.await?; diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index 3442cfe7ea..56702eb7b0 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -26,7 +26,7 @@ use tokio::{signal::ctrl_c, sync::RwLock}; use tokio_util::sync::CancellationToken; static NYM_API_URL: LazyLock<String> = LazyLock::new(|| { - std::env::var(NYM_API).unwrap_or_else(|_| panic!("{} env var not set", NYM_API)) + std::env::var(NYM_API).unwrap_or_else(|_| panic!("{NYM_API} env var not set")) }); static MIXNET_TIMEOUT: OnceCell<u64> = OnceCell::const_new(); @@ -48,10 +48,10 @@ async fn make_clients( ) { loop { let spawned_clients = clients.read().await.len(); - info!("Currently spawned clients: {}", spawned_clients); + info!("Currently spawned clients: {spawned_clients}"); // If we have enough clients, sleep for a minute and remove the oldest one if spawned_clients >= n_clients { - info!("New client will be spawned in {} seconds", lifetime); + info!("New client will be spawned in {lifetime} seconds"); tokio::time::sleep(tokio::time::Duration::from_secs(lifetime)).await; info!("Removing oldest client"); if let Some(dropped_client) = clients.write().await.pop_front() { @@ -74,7 +74,7 @@ async fn make_clients( let client = match make_client(topology.clone()).await { Ok(client) => client, Err(err) => { - warn!("{}, moving on", err); + warn!("{err}, moving on"); continue; } }; diff --git a/nym-node-status-api/nym-node-status-agent/src/main.rs b/nym-node-status-api/nym-node-status-agent/src/main.rs index d3078753fa..0415864d9f 100644 --- a/nym-node-status-api/nym-node-status-agent/src/main.rs +++ b/nym-node-status-api/nym-node-status-agent/src/main.rs @@ -51,7 +51,7 @@ pub(crate) fn setup_tracing() { "axum", ]; for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))); } filter = filter.add_directive(directive_checked("nym_bin_common=debug")); diff --git a/nym-node-status-api/nym-node-status-agent/src/probe.rs b/nym-node-status-api/nym-node-status-agent/src/probe.rs index 67e7e508c6..08d0a823aa 100644 --- a/nym-node-status-api/nym-node-status-agent/src/probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/probe.rs @@ -30,7 +30,7 @@ impl GwProbe { } Err(e) => { error!("Failed to stat binary at {}: {}", &self.path, e); - return format!("Failed to access binary: {}", e); + return format!("Failed to access binary: {e}"); } } } @@ -55,17 +55,17 @@ impl GwProbe { output.status.code().unwrap_or(-1), stderr ); - format!("Command failed: {}", stderr) + format!("Command failed: {stderr}") } } Err(e) => { error!("Failed to get command output: {}", e); - format!("Failed to get command output: {}", e) + format!("Failed to get command output: {e}") } }, Err(e) => { error!("Failed to spawn process: {}", e); - format!("Failed to spawn process: {}", e) + format!("Failed to spawn process: {e}") } } } diff --git a/nym-node-status-api/nym-node-status-api/build.rs b/nym-node-status-api/nym-node-status-api/build.rs index 1032fcb691..9da6d48d2a 100644 --- a/nym-node-status-api/nym-node-status-api/build.rs +++ b/nym-node-status-api/nym-node-status-api/build.rs @@ -13,7 +13,7 @@ const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let out_dir = read_env_var("OUT_DIR")?; - let database_path = format!("{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}?mode=rwc"); write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; let mut conn = SqliteConnection::connect(&database_path).await?; @@ -44,8 +44,7 @@ async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Resu let mut file = File::create("enter_db.sh").await?; let contents = format!( "#!/bin/bash\n\ - sqlite3 -init settings.sql {}/{}", - out_dir, db_filename, + sqlite3 -init settings.sql {out_dir}/{db_filename}", ); file.write_all(contents.as_bytes()).await?; diff --git a/nym-node-status-api/nym-node-status-api/src/http/error.rs b/nym-node-status-api/nym-node-status-api/src/http/error.rs index ce0e0f4060..0282958054 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/error.rs @@ -44,7 +44,7 @@ impl HttpError { pub(crate) fn no_delegations_for_node(node_id: NodeId) -> Self { Self { - message: format!("No delegation data for node_id={}", node_id), + message: format!("No delegation data for node_id={node_id}"), status: axum::http::StatusCode::NOT_FOUND, } } diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 2ddd8e19df..d99da2c698 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -230,7 +230,7 @@ impl DVpnGateway { fn to_percent(performance: u8) -> String { let fraction = performance as f32 / 100.0; - format!("{:.2}", fraction) + format!("{fraction:.2}") } #[cfg(test)] diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index 36c55a787a..548fc6206d 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -35,7 +35,7 @@ pub(crate) async fn start_http_api( .await; let router = router_builder.with_state(state); - let bind_addr = format!("0.0.0.0:{}", http_port); + let bind_addr = format!("0.0.0.0:{http_port}"); tracing::info!("Binding server to {bind_addr}"); let server = router.build_server(bind_addr).await?; diff --git a/nym-node-status-api/nym-node-status-api/src/logging.rs b/nym-node-status-api/nym-node-status-api/src/logging.rs index 67913d28ae..c4581af169 100644 --- a/nym-node-status-api/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/nym-node-status-api/src/logging.rs @@ -39,7 +39,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "hickory_resolver", ]; for crate_name in warn_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); } let log_level_hint = filter.max_level_hint(); diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 7461837fe0..0711299088 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -299,7 +299,7 @@ impl Monitor { let mut log_lines: Vec<String> = vec![]; for (key, value) in nodes_summary.iter() { - log_lines.push(format!("{} = {}", key, value)); + log_lines.push(format!("{key} = {value}")); } tracing::info!("Directory summary: \n{}", log_lines.join("\n")); diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs index fa44a9ef94..e36dc548f8 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs @@ -85,10 +85,7 @@ pub(crate) async fn try_queue_testrun( // save test run // let status = TestRunStatus::Queued as u32; - let log = format!( - "Test for {identity_key} requested at {} UTC\n\n", - timestamp_pretty - ); + let log = format!("Test for {identity_key} requested at {timestamp_pretty} UTC\n\n"); let id = sqlx::query!( "INSERT INTO testruns (gateway_id, status, ip_address, created_utc, log) VALUES (?, ?, ?, ?, ?)", diff --git a/nym-node-status-api/nym-node-status-client/src/lib.rs b/nym-node-status-api/nym-node-status-client/src/lib.rs index 39e802cde9..00e260e6b5 100644 --- a/nym-node-status-api/nym-node-status-client/src/lib.rs +++ b/nym-node-status-api/nym-node-status-client/src/lib.rs @@ -16,7 +16,7 @@ pub struct NsApiClient { impl NsApiClient { pub fn new(server_ip: &str, server_port: u16, auth_key: PrivateKey) -> Self { - let server_address = format!("{}:{}", server_ip, server_port); + let server_address = format!("{server_ip}:{server_port}"); let api = ApiPaths::new(server_address); let client = reqwest::Client::new(); diff --git a/nym-node/src/logging.rs b/nym-node/src/logging.rs index 332e274944..4852b68635 100644 --- a/nym-node/src/logging.rs +++ b/nym-node/src/logging.rs @@ -16,7 +16,7 @@ pub(crate) fn granual_filtered_env() -> anyhow::Result<tracing_subscriber::filte // these crates are more granularly filtered let filter_crates = ["defguard_wireguard_rs"]; for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); } Ok(filter) } diff --git a/nym-node/src/throughput_tester/global_stats.rs b/nym-node/src/throughput_tester/global_stats.rs index 97239ddcee..e095c702c5 100644 --- a/nym-node/src/throughput_tester/global_stats.rs +++ b/nym-node/src/throughput_tester/global_stats.rs @@ -150,9 +150,7 @@ impl GlobalStatsUpdater { info!("wrote global stats to {}", global.display()); for (sender_id, records) in self.records.iter() { - let output = self - .output_directory - .join(format!("sender{}.csv", sender_id)); + let output = self.output_directory.join(format!("sender{sender_id}.csv")); let mut writer = csv::Writer::from_path(&output)?; for record in records { writer.serialize(record)?; diff --git a/nym-statistics-api/src/http/server.rs b/nym-statistics-api/src/http/server.rs index 3aa57c7da6..8f84f3e4fb 100644 --- a/nym-statistics-api/src/http/server.rs +++ b/nym-statistics-api/src/http/server.rs @@ -19,7 +19,7 @@ pub(crate) async fn build_http_api( let state = AppState::new(storage, cached_network).await; let router = router_builder.with_state(state); - let bind_addr = format!("0.0.0.0:{}", http_port); + let bind_addr = format!("0.0.0.0:{http_port}"); tracing::info!("Binding server to {bind_addr}"); router.build_server(bind_addr).await diff --git a/nym-statistics-api/src/logging.rs b/nym-statistics-api/src/logging.rs index 0a5757fc08..39c2810226 100644 --- a/nym-statistics-api/src/logging.rs +++ b/nym-statistics-api/src/logging.rs @@ -33,7 +33,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "nym_http_api_client", ]; for crate_name in warn_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); } let log_level_hint = filter.max_level_hint(); diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 2a3692d056..a53976a960 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4229,6 +4229,7 @@ version = "0.1.0" dependencies = [ "bincode", "serde", + "serde_json", "tracing", ] diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 548f135173..b5c1ccdfba 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -145,8 +145,8 @@ impl Config { .map_err(io::Error::other) .map(|toml| fs::write(location.clone(), toml)) { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + Ok(_) => log::debug!("Writing to: {location:#?}"), + Err(err) => log::warn!("Failed to write to {location:#?}: {err}"), } } @@ -165,8 +165,8 @@ impl Config { .map_err(io::Error::other) .map(|toml| fs::write(location.clone(), toml)) { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + Ok(_) => log::debug!("Writing to: {location:#?}"), + Err(err) => log::warn!("Failed to write to {location:#?}: {err}"), } } Ok(()) @@ -178,11 +178,11 @@ impl Config { let file = Self::config_file_path(None); match load_from_file::<GlobalConfig>(file.clone()) { Ok(global) => { - log::debug!("Loaded from file {:#?}", file); + log::debug!("Loaded from file {file:#?}"); Some(global) } Err(err) => { - log::trace!("Not loading {:#?}: {err}", file); + log::trace!("Not loading {file:#?}: {err}"); None } } @@ -194,10 +194,10 @@ impl Config { let file = Self::config_file_path(Some(network)); match load_from_file::<NetworkConfig>(file.clone()) { Ok(config) => { - log::trace!("Loaded from file {:#?}", file); + log::trace!("Loaded from file {file:#?}"); networks.insert(network.as_key(), config); } - Err(err) => log::trace!("Not loading {:#?}: {err}", file), + Err(err) => log::trace!("Not loading {file:#?}: {err}"), }; } diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 6f75b6e5fa..c0f14a65bd 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -33,7 +33,7 @@ pub async fn get_selected_nyxd_url( ) -> Result<Option<String>, BackendError> { let state = state.read().await; let url = state.get_selected_nyxd_url(&network).map(String::from); - log::info!("Selected nyxd url for {network}: {:?}", url); + log::info!("Selected nyxd url for {network}: {url:?}"); Ok(url) } @@ -44,7 +44,7 @@ pub async fn get_default_nyxd_url( ) -> Result<String, BackendError> { let state = state.read().await; let url = state.get_default_nyxd_url(&network).map(String::from); - log::info!("Default nyxd url for {network}: {:?}", url); + log::info!("Default nyxd url for {network}: {url:?}"); url.ok_or_else(|| BackendError::WalletNoDefaultValidator) } diff --git a/nym-wallet/src-tauri/src/operations/app/link.rs b/nym-wallet/src-tauri/src/operations/app/link.rs index fa41aa8f19..d238242633 100644 --- a/nym-wallet/src-tauri/src/operations/app/link.rs +++ b/nym-wallet/src-tauri/src/operations/app/link.rs @@ -2,10 +2,10 @@ use tauri_plugin_opener::OpenerExt; #[tauri::command] pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> { - println!("Opening URL: {}", url); + println!("Opening URL: {url}"); match app_handle.opener().open_url(&url, None::<&str>) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to open URL: {}", err)), + Err(err) => Err(format!("Failed to open URL: {err}")), } } diff --git a/nym-wallet/src-tauri/src/operations/app/version.rs b/nym-wallet/src-tauri/src/operations/app/version.rs index d904449cf0..d69a80f5e3 100644 --- a/nym-wallet/src-tauri/src/operations/app/version.rs +++ b/nym-wallet/src-tauri/src/operations/app/version.rs @@ -10,13 +10,13 @@ pub async fn check_version(handle: tauri::AppHandle) -> Result<AppVersion, Backe log::info!(">>> Getting app version info"); let updater = handle.updater().map_err(|e| { - log::error!("Failed to get updater: {}", e); + log::error!("Failed to get updater: {e}"); BackendError::CheckAppVersionError })?; // Then check for updates let update_info = updater.check().await.map_err(|e| { - log::error!("An error occurred while checking for app update {}", e); + log::error!("An error occurred while checking for app update {e}"); BackendError::CheckAppVersionError })?; @@ -35,10 +35,7 @@ pub async fn check_version(handle: tauri::AppHandle) -> Result<AppVersion, Backe } else { // No update available let current_version = handle.package_info().version.to_string(); - log::debug!( - "<<< update available: [false], current version {}", - current_version - ); + log::debug!("<<< update available: [false], current version {current_version}"); Ok(AppVersion { current_version: current_version.clone(), latest_version: current_version, diff --git a/nym-wallet/src-tauri/src/operations/app/window.rs b/nym-wallet/src-tauri/src/operations/app/window.rs index 212af30fb1..cb60f8192b 100644 --- a/nym-wallet/src-tauri/src/operations/app/window.rs +++ b/nym-wallet/src-tauri/src/operations/app/window.rs @@ -25,7 +25,7 @@ async fn create_window( try_close_window_label: &str, ) -> Result<(), BackendError> { // create the new window first, to stop the app process from exiting - log::info!("Creating {} window...", new_window_label); + log::info!("Creating {new_window_label} window..."); match tauri::WebviewWindowBuilder::new( &app_handle, new_window_label, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 3750e21aee..3319363be1 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -108,7 +108,7 @@ async fn _connect_with_mnemonic( "{}", state.get_config_validator_entries(network).format(",\n") ); - log::debug!("List of validators for {network}: [\n{}\n]", f,); + log::debug!("List of validators for {network}: [\n{f}\n]",); } state.config().clone() @@ -598,7 +598,7 @@ pub async fn list_accounts( address: account.addresses[&network].to_string(), }) .map(|account| { - log::trace!("{:?}", account); + log::trace!("{account:?}"); account }) .collect(); diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index ee9e0e3742..881b54aa3c 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -71,7 +71,7 @@ pub async fn bond_gateway( .bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -84,10 +84,10 @@ pub async fn unbond_gateway( ) -> Result<TransactionExecuteResult, BackendError> { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!(">>> Unbond gateway, fee = {:?}", fee); + log::info!(">>> Unbond gateway, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_gateway(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -136,7 +136,7 @@ pub async fn bond_mixnode( .bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -184,7 +184,7 @@ pub async fn bond_nymnode( .bond_nymnode(nymnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -232,7 +232,7 @@ pub async fn update_pledge( }; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -249,10 +249,7 @@ pub async fn pledge_more( let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Pledge more, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", - additional_pledge, - additional_pledge_base, - fee, + ">>> Pledge more, additional_pledge_display = {additional_pledge}, additional_pledge_base = {additional_pledge_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -260,7 +257,7 @@ pub async fn pledge_more( .pledge_more(additional_pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -276,10 +273,7 @@ pub async fn decrease_pledge( let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Decrease pledge, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}", - decrease_by, - decrease_by_base, - fee, + ">>> Decrease pledge, pledge_decrease_display = {decrease_by}, pledge_decrease_base = {decrease_by_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -287,7 +281,7 @@ pub async fn decrease_pledge( .decrease_pledge(decrease_by_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -300,10 +294,10 @@ pub async fn unbond_mixnode( ) -> Result<TransactionExecuteResult, BackendError> { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!(">>> Unbond mixnode, fee = {:?}", fee); + log::info!(">>> Unbond mixnode, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_mixnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -319,7 +313,7 @@ pub async fn unbond_nymnode( log::info!(">>> Unbond NymNode, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_nymnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -347,7 +341,7 @@ pub async fn update_mixnode_cost_params( .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -372,7 +366,7 @@ pub async fn update_mixnode_config( .update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -397,7 +391,7 @@ pub async fn update_gateway_config( .update_gateway_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -420,14 +414,14 @@ pub async fn get_mixnode_avg_uptime( match res.mixnode_details { Some(details) => { let id = details.mix_id(); - log::trace!(" >>> Get average uptime percentage: mix_id = {}", id); + log::trace!(" >>> Get average uptime percentage: mix_id = {id}"); let avg_uptime_percent = client .nym_api .get_mixnode_avg_uptime(id) .await .ok() .map(|r| r.avg_uptime); - log::trace!(" <<< {:?}", avg_uptime_percent); + log::trace!(" <<< {avg_uptime_percent:?}"); Ok(avg_uptime_percent) } None => Ok(None), @@ -461,7 +455,7 @@ pub async fn mixnode_bond_details( &r.bond_information.mix_node.identity_key )) ); - log::trace!("<<< {:?}", details); + log::trace!("<<< {details:?}"); Ok(details) } @@ -490,7 +484,7 @@ pub async fn gateway_bond_details( "<<< identity_key = {:?}", res.as_ref().map(|r| r.gateway.identity_key.to_string()) ); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } @@ -521,7 +515,7 @@ pub async fn nym_node_bond_details( &r.bond_information.node.identity_key )) ); - log::trace!("<<< {:?}", details); + log::trace!("<<< {details:?}"); Ok(details) } @@ -530,7 +524,7 @@ pub async fn get_pending_operator_rewards( address: String, state: tauri::State<'_, WalletState>, ) -> Result<DecCoin, BackendError> { - log::info!(">>> Get pending operator rewards for {}", address); + log::info!(">>> Get pending operator rewards for {address}"); let guard = state.read().await; let res = guard .current_client()? @@ -554,11 +548,7 @@ pub async fn get_pending_operator_rewards( .transpose()? .unwrap_or_else(|| guard.default_zero_mix_display_coin()); - log::info!( - "<<< rewards_base = {:?}, rewards_display = {}", - base_coin, - display_coin - ); + log::info!("<<< rewards_base = {base_coin:?}, rewards_display = {display_coin}"); Ok(display_coin) } @@ -637,7 +627,7 @@ pub async fn migrate_legacy_mixnode( let res = client.nyxd.migrate_legacy_mixnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -657,7 +647,7 @@ pub async fn migrate_legacy_gateway( let res = client.nyxd.migrate_legacy_gateway(None, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -678,7 +668,7 @@ pub async fn update_nymnode_config( .update_nymnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -705,7 +695,7 @@ pub async fn update_nymnode_cost_params( .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 8fc47187d0..5275436dcd 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -62,10 +62,7 @@ pub async fn get_pending_delegation_events( "<<< {} pending delegation events", client_specific_events.len() ); - log::trace!( - "<<< pending delegation events = {:?}", - client_specific_events - ); + log::trace!("<<< pending delegation events = {client_specific_events:?}"); Ok(client_specific_events) } @@ -83,15 +80,11 @@ pub async fn delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode: mix_id = {}, display_amount = {}, base_amount = {}, fee = {:?}", - mix_id, - amount, - delegation_base, - fee, + ">>> Delegate to mixnode: mix_id = {mix_id}, display_amount = {amount}, base_amount = {delegation_base}, fee = {fee:?}", ); let res = client.nyxd.delegate(mix_id, delegation_base, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -106,14 +99,10 @@ pub async fn undelegate_from_mixnode( let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Undelegate from mixnode: mix_id = {}, fee = {:?}", - mix_id, - fee - ); + log::info!(">>> Undelegate from mixnode: mix_id = {mix_id}, fee = {fee:?}"); let res = guard.current_client()?.nyxd.undelegate(mix_id, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -128,11 +117,7 @@ pub async fn undelegate_all_from_mixnode( state: tauri::State<'_, WalletState>, ) -> Result<Vec<TransactionExecuteResult>, BackendError> { log::info!( - ">>> Undelegate all from mixnode: mix_id = {}, uses_vesting_contract_tokens = {}, fee_liquid = {:?}, fee_vesting = {:?}", - mix_id, - uses_vesting_contract_tokens, - fee_liquid, - fee_vesting, + ">>> Undelegate all from mixnode: mix_id = {mix_id}, uses_vesting_contract_tokens = {uses_vesting_contract_tokens}, fee_liquid = {fee_liquid:?}, fee_vesting = {fee_vesting:?}", ); let mut res: Vec<TransactionExecuteResult> = vec![undelegate_from_mixnode(mix_id, fee_liquid, state.clone()).await?]; @@ -178,7 +163,7 @@ pub(crate) async fn get_node_information( let str_err = format!( "Failed to get legacy mixnode details for node_id = {node_id}. Error: {err}", ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); })? .mixnode_details; @@ -222,7 +207,7 @@ pub async fn get_all_mix_delegations( .get_all_delegator_delegations(&address) .await .tap_err(|err| { - log::error!(" <<< Failed to get delegations. Error: {}", err); + log::error!(" <<< Failed to get delegations. Error: {err}"); })?; log::info!(" <<< {} delegations", delegations.len()); @@ -230,7 +215,7 @@ pub async fn get_all_mix_delegations( get_pending_delegation_events(state.clone()) .await .tap_err(|err| { - log::error!(" <<< Failed to get pending delegations. Error: {}", err); + log::error!(" <<< Failed to get pending delegations. Error: {err}"); })?; log::info!( @@ -276,7 +261,7 @@ pub async fn get_all_mix_delegations( "Failed to get operator rewards as a display coin for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -295,7 +280,7 @@ pub async fn get_all_mix_delegations( "Failed to get delegator rewards as a display coin for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -314,12 +299,12 @@ pub async fn get_all_mix_delegations( "Failed to mixnode cost params for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); - log::trace!(" >>> Get accumulated rewards: address = {}", address); + log::trace!(" >>> Get accumulated rewards: address = {address}"); let pending_reward = client .nyxd .get_pending_delegator_reward(&address, d.mix_id, d.proxy.clone()) @@ -329,7 +314,7 @@ pub async fn get_all_mix_delegations( "Failed to get accumulated rewards for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -340,15 +325,11 @@ pub async fn get_all_mix_delegations( .attempt_convert_to_display_dec_coin(reward.clone().into()) .tap_err(|err| { let str_err = format!("Failed to get convert reward to a display coin for mix_id = {}. Error: {}", d.mix_id, err); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .ok(); - log::trace!( - " <<< rewards = {:?}, amount = {:?}", - pending_reward, - amount - ); + log::trace!(" <<< rewards = {pending_reward:?}, amount = {amount:?}"); amount } None => { @@ -367,7 +348,7 @@ pub async fn get_all_mix_delegations( "Failed to get stake saturation for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or(MixStakeSaturationResponse { @@ -375,7 +356,7 @@ pub async fn get_all_mix_delegations( uncapped_saturation: None, current_saturation: None, }); - log::trace!(" <<< {:?}", stake_saturation); + log::trace!(" <<< {stake_saturation:?}"); log::trace!( " >>> Get average uptime percentage: mix_iid = {}", @@ -391,7 +372,7 @@ pub async fn get_all_mix_delegations( "Failed to get current node performance for node_id = {}. Error: {err}", d.mix_id ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .ok() @@ -400,7 +381,7 @@ pub async fn get_all_mix_delegations( // convert to old u8 let current_uptime = current_performance.map(|p| (p * 100.) as u8); - log::trace!(" <<< {:?}", current_uptime); + log::trace!(" <<< {current_uptime:?}"); log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", @@ -415,19 +396,17 @@ pub async fn get_all_mix_delegations( // Check if the error is related to height not being available (pruning) if error_message.contains("height") && error_message.contains("not available") { let str_err = "Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations."; - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err.to_string()); } else { let str_err = format!("Failed to get block timestamp for height = {} for delegation to mix_id = {}. Error: {}", d.height, d.mix_id, err); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); } }).ok(); let delegated_on_iso_datetime = timestamp.map(|ts| ts.to_rfc3339()); log::trace!( - " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", - timestamp, - delegated_on_iso_datetime + " <<< timestamp = {timestamp:?}, delegated_on_iso_datetime = {delegated_on_iso_datetime:?}" ); let pending_events = filter_pending_events(d.mix_id, &pending_events_for_account); @@ -466,7 +445,7 @@ pub async fn get_all_mix_delegations( }, }) } - log::trace!("<<< {:?}", with_everything); + log::trace!("<<< {with_everything:?}"); Ok(with_everything) } @@ -490,11 +469,7 @@ pub async fn get_pending_delegator_rewards( proxy: Option<String>, state: tauri::State<'_, WalletState>, ) -> Result<DecCoin, BackendError> { - log::info!( - ">>> Get pending delegator rewards: mix_id = {}, proxy = {:?}", - mix_id, - proxy - ); + log::info!(">>> Get pending delegator rewards: mix_id = {mix_id}, proxy = {proxy:?}"); let guard = state.read().await; let res = guard .current_client()? @@ -518,11 +493,7 @@ pub async fn get_pending_delegator_rewards( .transpose()? .unwrap_or_else(|| guard.default_zero_mix_display_coin()); - log::info!( - "<<< rewards_base = {:?}, rewards_display = {}", - base_coin, - display_coin - ); + log::info!("<<< rewards_base = {base_coin:?}, rewards_display = {display_coin}"); Ok(display_coin) } @@ -554,7 +525,7 @@ pub async fn get_delegation_summary( total_delegations, total_rewards ); - log::trace!("<<< {:?}", delegations); + log::trace!("<<< {delegations:?}"); Ok(DelegationsSummaryResponse { delegations, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs index 05151d8a53..8268f44242 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs @@ -14,7 +14,7 @@ pub async fn get_current_interval( ) -> Result<Interval, BackendError> { log::info!(">>> Get current interval"); let res = nyxd_client!(state).get_current_interval_details().await?; - log::info!("<<< current interval = {:?}", res); + log::info!("<<< current interval = {res:?}"); Ok(res.interval.into()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 4563005b88..b32a4224b3 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -23,7 +23,7 @@ pub async fn claim_operator_reward( .withdraw_operator_reward(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -44,7 +44,7 @@ pub async fn claim_delegator_reward( .withdraw_delegator_reward(node_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -87,9 +87,7 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let did_delegate_with_vesting_contract = vesting_delegation.delegation.is_some(); log::trace!( - "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", - did_delegate_with_mixnet_contract, - did_delegate_with_vesting_contract + "<<< Delegations done with: mixnet contract = {did_delegate_with_mixnet_contract}, vesting contract = {did_delegate_with_vesting_contract}" ); let mut res: Vec<TransactionExecuteResult> = vec![]; @@ -99,7 +97,7 @@ pub async fn claim_locked_and_unlocked_delegator_reward( if did_delegate_with_vesting_contract { res.push(vesting_claim_delegator_reward(node_id, fee, state).await?); } - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index 9e1ae7849c..5a313608ca 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -20,12 +20,7 @@ pub async fn send( let from_address = guard.current_client()?.nyxd.address().to_string(); let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Send: display_amount = {}, base_amount = {}, from = {}, to = {}, fee = {:?}", - amount, - amount_base, - from_address, - to_address, - fee, + ">>> Send: display_amount = {amount}, base_amount = {amount_base}, from = {from_address}, to = {to_address}, fee = {fee:?}", ); let raw_res = guard .current_client()? @@ -38,6 +33,6 @@ pub async fn send( TransactionDetails::new(amount, from_address, to_address.to_string()), fee_amount, ); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index 59da5cae36..e93edd57f4 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -40,7 +40,7 @@ pub async fn sign( signature_as_hex: signature.to_string(), }; let output_json = json!(output).to_string(); - log::info!(">>> Signing data {}", output_json); + log::info!(">>> Signing data {output_json}"); Ok(output_json) } @@ -48,17 +48,17 @@ async fn get_pubkey_from_account_address( address: &AccountId, state: &tauri::State<'_, WalletState>, ) -> Result<PublicKey, BackendError> { - log::info!("Getting public key for address {} from chain...", address); + log::info!("Getting public key for address {address} from chain..."); let guard = state.read().await; let client = guard.current_client()?; let account = client.nyxd.get_account(address).await?.ok_or_else(|| { - log::error!("No account associated with address {}", address); + log::error!("No account associated with address {address}"); BackendError::SignatureError(format!("No account associated with address {address}")) })?; let base_account = account.try_get_base_account()?; base_account.pubkey.ok_or_else(|| { - log::error!("No pubkey found for address {}", address); + log::error!("No pubkey found for address {address}"); BackendError::SignatureError(format!("No pubkey found for address {address}")) }) } @@ -125,7 +125,7 @@ pub async fn verify( )); } - log::info!("<<< Verifying signature [{}]", signature_as_hex); + log::info!("<<< Verifying signature [{signature_as_hex}]"); let verifying_key = VerifyingKey::from_sec1_bytes(&public_key.to_bytes())?; let signature = Signature::from_str(&signature_as_hex)?; let message_as_bytes = message.into_bytes(); diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index eb1b4bb1cf..3866062e2f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -114,17 +114,11 @@ pub async fn simulate_update_pledge( match new_pledge.amount.cmp(¤t_pledge.amount) { Ordering::Greater => { - log::info!( - "Simulate pledge increase, calculated additional pledge {}", - dec_delta, - ); + log::info!("Simulate pledge increase, calculated additional pledge {dec_delta}",); simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(dec_delta), &state).await } Ordering::Less => { - log::info!( - "Simulate pledge reduction, calculated reduction pledge {}", - dec_delta, - ); + log::info!("Simulate pledge reduction, calculated reduction pledge {dec_delta}",); simulate_mixnet_operation( ExecuteMsg::DecreasePledge { decrease_by: guard.attempt_convert_to_base_coin(dec_delta)?.into(), diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 9578575c3f..28752f4bad 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -114,8 +114,7 @@ pub async fn simulate_vesting_update_pledge( })? .into(); log::info!( - ">>> Simulate pledge more, calculated additional pledge {}", - additional_pledge, + ">>> Simulate pledge more, calculated additional pledge {additional_pledge}", ); simulate_vesting_operation( ExecuteMsg::PledgeMore { @@ -134,8 +133,7 @@ pub async fn simulate_vesting_update_pledge( })? .into(); log::info!( - ">>> Simulate decrease pledge, calculated decrease pledge {}", - decrease_pledge, + ">>> Simulate decrease pledge, calculated decrease pledge {decrease_pledge}", ); simulate_vesting_operation( ExecuteMsg::DecreasePledge { diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index dfa7aec1f9..9a97b2bfe6 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -48,7 +48,7 @@ pub async fn vesting_bond_gateway( .vesting_bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -61,13 +61,10 @@ pub async fn vesting_unbond_gateway( ) -> Result<TransactionExecuteResult, BackendError> { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Unbond gateway bonded with locked tokens, fee = {:?}", - fee - ); + log::info!(">>> Unbond gateway bonded with locked tokens, fee = {fee:?}"); let res = nyxd_client!(state).vesting_unbond_gateway(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -118,7 +115,7 @@ pub async fn vesting_bond_mixnode( .vesting_bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -144,9 +141,7 @@ pub async fn vesting_update_pledge( let res = match new_pledge.amount.cmp(¤t_pledge.amount) { Ordering::Greater => { log::info!( - "Pledge increase with locked tokens, calculated additional pledge {}, fee = {:?}", - dec_delta, - fee, + "Pledge increase with locked tokens, calculated additional pledge {dec_delta}, fee = {fee:?}", ); guard .current_client()? @@ -156,9 +151,7 @@ pub async fn vesting_update_pledge( } Ordering::Less => { log::info!( - "Pledge reduction with locked tokens, calculated reduction pledge {}, fee = {:?}", - dec_delta, - fee, + "Pledge reduction with locked tokens, calculated reduction pledge {dec_delta}, fee = {fee:?}", ); guard .current_client()? @@ -170,7 +163,7 @@ pub async fn vesting_update_pledge( }; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -187,10 +180,7 @@ pub async fn vesting_pledge_more( let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Pledge more with locked tokens, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", - additional_pledge, - additional_pledge_base, - fee, + ">>> Pledge more with locked tokens, additional_pledge_display = {additional_pledge}, additional_pledge_base = {additional_pledge_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -198,7 +188,7 @@ pub async fn vesting_pledge_more( .vesting_pledge_more(additional_pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -214,10 +204,7 @@ pub async fn vesting_decrease_pledge( let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Decrease pledge with locked tokens, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}", - decrease_by, - decrease_by_base, - fee, + ">>> Decrease pledge with locked tokens, pledge_decrease_display = {decrease_by}, pledge_decrease_base = {decrease_by_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -225,7 +212,7 @@ pub async fn vesting_decrease_pledge( .vesting_decrease_pledge(decrease_by_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -238,17 +225,14 @@ pub async fn vesting_unbond_mixnode( ) -> Result<TransactionExecuteResult, BackendError> { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", - fee - ); + log::info!(">>> Unbond mixnode bonded with locked tokens, fee = {fee:?}"); let res = guard .current_client()? .nyxd .vesting_unbond_mixnode(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -265,10 +249,7 @@ pub async fn withdraw_vested_coins( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Withdraw vested liquid coins: amount_base = {}, amount_base = {}, fee = {:?}", - amount, - amount_base, - fee + ">>> Withdraw vested liquid coins: amount_base = {amount}, amount_base = {amount_base}, fee = {fee:?}" ); let res = guard .current_client()? @@ -276,7 +257,7 @@ pub async fn withdraw_vested_coins( .withdraw_vested_coins(amount_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -304,7 +285,7 @@ pub async fn vesting_update_mixnode_cost_params( .vesting_update_mixnode_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -329,7 +310,7 @@ pub async fn vesting_update_mixnode_config( .vesting_update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -354,7 +335,7 @@ pub async fn vesting_update_gateway_config( .vesting_update_gateway_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index bbb1d8df11..ed896c0322 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -20,11 +20,7 @@ pub async fn vesting_delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode with locked tokens: mix_id = {}, amount_display = {}, amount_base = {}, fee = {:?}", - mix_id, - amount, - delegation, - fee + ">>> Delegate to mixnode with locked tokens: mix_id = {mix_id}, amount_display = {amount}, amount_base = {delegation}, fee = {fee:?}" ); let res = guard .current_client()? @@ -32,7 +28,7 @@ pub async fn vesting_delegate_to_mixnode( .vesting_delegate_to_mixnode(mix_id, delegation, None, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -47,9 +43,7 @@ pub async fn vesting_undelegate_from_mixnode( let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {}, fee = {:?}", - mix_id, - fee, + ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {mix_id}, fee = {fee:?}", ); let res = guard .current_client()? @@ -57,7 +51,7 @@ pub async fn vesting_undelegate_from_mixnode( .vesting_undelegate_from_mixnode(mix_id, None, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index 00257464b1..6d99641f87 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -25,7 +25,7 @@ pub async fn migrate_vested_mixnode( .migrate_vested_mixnode(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -52,7 +52,7 @@ pub async fn migrate_vested_delegations( .get_all_delegator_delegations(&address) .await .inspect_err(|err| { - log::error!(" <<< Failed to get delegations. Error: {}", err); + log::error!(" <<< Failed to get delegations. Error: {err}"); })?; log::info!(" <<< {} delegations", delegations.len()); @@ -91,6 +91,6 @@ pub async fn migrate_vested_delegations( .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result(res, None)?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index f3377d5ed5..750d03caee 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -146,7 +146,7 @@ pub(crate) async fn vesting_start_time( .vesting_start_time(vesting_account_address) .await? .seconds(); - log::info!("<<< vesting start time = {}", res); + log::info!("<<< vesting start time = {res}"); Ok(res) } @@ -160,7 +160,7 @@ pub(crate) async fn vesting_end_time( .vesting_end_time(vesting_account_address) .await? .seconds(); - log::info!("<<< vesting end time = {}", res); + log::info!("<<< vesting end time = {res}"); Ok(res) } @@ -180,7 +180,7 @@ pub(crate) async fn original_vesting( .await?; let res = OriginalVestingResponse::from_vesting_contract(res, reg)?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -347,7 +347,7 @@ pub(crate) async fn vesting_get_mixnode_pledge( .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) .transpose()?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -368,7 +368,7 @@ pub(crate) async fn vesting_get_gateway_pledge( .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) .transpose()?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -381,7 +381,7 @@ pub(crate) async fn get_current_vesting_period( let res = nyxd_client!(state) .get_current_vesting_period(address) .await?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -397,6 +397,6 @@ pub(crate) async fn get_account_info( let vesting_account = guard.current_client()?.nyxd.get_account(address).await?; let res = VestingAccountInfo::from_vesting_contract(vesting_account, res)?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index 7cf2155cdb..f41fee9055 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -22,7 +22,7 @@ pub async fn vesting_claim_operator_reward( .vesting_withdraw_operator_reward(None) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -34,10 +34,7 @@ pub async fn vesting_claim_delegator_reward( fee: Option<Fee>, state: tauri::State<'_, WalletState>, ) -> Result<TransactionExecuteResult, BackendError> { - log::info!( - ">>> Vesting account: claim delegator reward: mix_id = {}", - mix_id - ); + log::info!(">>> Vesting account: claim delegator reward: mix_id = {mix_id}"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard @@ -46,7 +43,7 @@ pub async fn vesting_claim_delegator_reward( .vesting_withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 973172f4fc..42f9f080fe 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -286,7 +286,7 @@ fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendErro let mut stored_wallet = load_existing_wallet_at_file(filepath)?; if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); return Ok(fs::remove_file(filepath)?); } @@ -295,7 +295,7 @@ fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendErro .ok_or(BackendError::WalletNoSuchLoginId)?; if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); Ok(fs::remove_file(filepath)?) } else { write_to_file(filepath, &stored_wallet) @@ -345,7 +345,7 @@ fn _archive_wallet_file(path: &Path) -> Result<(), BackendError> { additional_number += 1; } else { if let Some(new_path) = new_path.to_str() { - log::info!("Archived to: {}", new_path); + log::info!("Archived to: {new_path}"); } else { log::warn!("Archived wallet file to filename that is not a valid UTF-8 string"); } @@ -363,17 +363,14 @@ pub(crate) fn archive_wallet_file() -> Result<(), BackendError> { if filepath.exists() { if let Some(filepath) = filepath.to_str() { - log::info!("Archiving wallet file: {}", filepath); + log::info!("Archiving wallet file: {filepath}"); } else { log::info!("Archiving wallet file"); } _archive_wallet_file(&filepath) } else { if let Some(filepath) = filepath.to_str() { - log::info!( - "Skipping archiving wallet file, as it's not found: {}", - filepath - ); + log::info!("Skipping archiving wallet file, as it's not found: {filepath}"); } else { log::info!("Skipping archiving wallet file, as it's not found"); } @@ -430,7 +427,7 @@ fn remove_account_from_login_at_file( // Remove the file, or write the new file if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); Ok(fs::remove_file(filepath)?) } else { write_to_file(filepath, &stored_wallet) diff --git a/nyx-chain-watcher/build.rs b/nyx-chain-watcher/build.rs index 5cf16e56f8..a6dbb3c105 100644 --- a/nyx-chain-watcher/build.rs +++ b/nyx-chain-watcher/build.rs @@ -14,7 +14,7 @@ async fn main() -> Result<()> { } let db_path_str = db_path.display().to_string().replace('\\', "/"); - let db_url = format!("sqlite:{}", db_path_str); + let db_url = format!("sqlite:{db_path_str}"); // Ensure database file is created with proper permissions let connect_options = SqliteConnectOptions::from_str(&db_url)? @@ -30,7 +30,7 @@ async fn main() -> Result<()> { // Force SQLx to prepare all queries during build println!("cargo:rustc-env=SQLX_OFFLINE=true"); - println!("cargo:rustc-env=DATABASE_URL={}", db_url); + println!("cargo:rustc-env=DATABASE_URL={db_url}"); // Add rerun-if-changed directives println!("cargo:rerun-if-changed=migrations"); @@ -46,7 +46,7 @@ fn export_db_variables(db_url: &str) -> Result<()> { let mut file = File::create(".env")?; for (var, value) in map.iter() { - writeln!(file, "{}={}", var, value)?; + writeln!(file, "{var}={value}")?; } Ok(()) diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 84d9f10f3b..150738dfb6 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -133,7 +133,7 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa std::fs::create_dir_all(parent)?; } - let connection_url = format!("sqlite://{}?mode=rwc", db_path); + let connection_url = format!("sqlite://{db_path}?mode=rwc"); let storage = db::Storage::init(connection_url).await?; let watcher_pool = storage.pool_owned(); diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index 96e7d47f95..4216441d66 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -34,7 +34,7 @@ pub(crate) async fn build_http_api( ); let router = router_builder.with_state(state); - let bind_addr = format!("0.0.0.0:{}", http_port); + let bind_addr = format!("0.0.0.0:{http_port}"); let server = router.build_server(bind_addr).await?; Ok(server) } diff --git a/nyx-chain-watcher/src/logging.rs b/nyx-chain-watcher/src/logging.rs index 74479e3c7d..1c8cbeebb1 100644 --- a/nyx-chain-watcher/src/logging.rs +++ b/nyx-chain-watcher/src/logging.rs @@ -36,7 +36,7 @@ pub(crate) fn setup_tracing_logger() { "axum", ]; for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))); } log_builder.with_env_filter(filter).init(); diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 46eb67d3ef..9c67bbf898 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -18,9 +18,9 @@ path = "src/tcp_proxy/bin/proxy_client.rs" async-trait = { workspace = true } bip39 = { workspace = true } nym-client-core = { path = "../../../common/client-core", features = [ - "fs-credentials-storage", - "fs-surb-storage", - "fs-gateways-storage", + "fs-credentials-storage", + "fs-surb-storage", + "fs-gateways-storage", ] } nym-crypto = { path = "../../../common/crypto" } nym-gateway-requests = { path = "../../../common/gateway-requests" } @@ -36,14 +36,14 @@ nym-task = { path = "../../../common/task" } nym-topology = { path = "../../../common/topology" } nym-socks5-client-core = { path = "../../../common/socks5-client-core" } nym-validator-client = { path = "../../../common/client-libs/validator-client", features = [ - "http-client", + "http-client", ] } nym-socks5-requests = { path = "../../../common/socks5/requests" } nym-ordered-buffer = { path = "../../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../../service-providers/common" } nym-sphinx-addressing = { path = "../../../common/nymsphinx/addressing" } nym-bin-common = { path = "../../../common/bin-common", features = [ - "basic_tracing", + "basic_tracing", ] } bytecodec = { workspace = true } httpcodec = { workspace = true } @@ -80,6 +80,7 @@ dotenvy = { workspace = true } reqwest = { workspace = true, features = ["json", "socks"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } +time = { workspace = true } nym-bin-common = { path = "../../../common/bin-common", features = ["basic_tracing"] } # extra dependencies for libp2p examples diff --git a/sdk/rust/nym-sdk/examples/surb_reply.rs b/sdk/rust/nym-sdk/examples/surb_reply.rs index 74f9162470..6011181f0f 100644 --- a/sdk/rust/nym-sdk/examples/surb_reply.rs +++ b/sdk/rust/nym-sdk/examples/surb_reply.rs @@ -55,8 +55,7 @@ async fn main() { // parse sender_tag: we will use this to reply to sender without needing their Nym address let return_recipient: AnonymousSenderTag = message[0].sender_tag.unwrap(); println!( - "\nReceived the following message: {} \nfrom sender with surb bucket {}", - parsed, return_recipient + "\nReceived the following message: {parsed} \nfrom sender with surb bucket {return_recipient}" ); // reply to self with it: note we use `send_str_reply` instead of `send_str` diff --git a/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs b/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs index 86cfbf70cd..adce2e263f 100644 --- a/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs +++ b/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs @@ -45,7 +45,7 @@ async fn main() -> anyhow::Result<()> { let server_port = env::args() .nth(1) .expect("Server listen port not specified"); - let upstream_tcp_addr = format!("127.0.0.1:{}", server_port); + let upstream_tcp_addr = format!("127.0.0.1:{server_port}"); // This dir gets cleaned up at the end: NOTE if you switch env between tests without letting the file do the automatic cleanup, make sure to manually remove this directory up before running again, otherwise your client will attempt to use these keys for the new env let home_dir = dirs::home_dir().expect("Unable to get home directory"); @@ -155,7 +155,7 @@ async fn main() -> anyhow::Result<()> { // The assumption regarding integration is that you know what you're sending, and will do proper // framing before and after, know what data types you're expecting, etc; the proxies are just piping bytes // back and forth using tokio's `Bytecodec` under the hood. - let local_tcp_addr = format!("127.0.0.1:{}", client_port); + let local_tcp_addr = format!("127.0.0.1:{client_port}"); let stream = TcpStream::connect(local_tcp_addr).await?; let (read, mut write) = stream.into_split(); diff --git a/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs b/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs index ff7b8b8198..76209b1792 100644 --- a/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs +++ b/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs @@ -47,7 +47,7 @@ impl fmt::Debug for ClientPool { "client_pool_reserve_number", &self.client_pool_reserve_number, ) - .field("clients", &format_args!("[{}]", clients_debug)); + .field("clients", &format_args!("[{clients_debug}]")); debug_struct.finish() } } diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 47e958cb38..90d1b218b6 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -510,7 +510,7 @@ where Ok(active) => { if let Some(active) = active.registration { let id = active.details.gateway_id(); - debug!("currently selected gateway: {0}", id); + debug!("currently selected gateway: {id}"); } } } diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index caa0f7d3e4..fb940c5b4d 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -226,14 +226,14 @@ impl MixnetClient { log::debug!("Sending forget me request: {:?}", self.forget_me); match self.send_forget_me().await { Ok(_) => (), - Err(e) => error!("Failed to send forget me request: {}", e), + Err(e) => error!("Failed to send forget me request: {e}"), }; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } else if self.remember_me.stats() { log::debug!("Sending remember me request: {:?}", self.remember_me); match self.send_remember_me().await { Ok(_) => (), - Err(e) => error!("Failed to send remember me request: {}", e), + Err(e) => error!("Failed to send remember me request: {e}"), }; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } @@ -255,7 +255,7 @@ impl MixnetClient { match self.client_request_sender.send(client_request).await { Ok(_) => Ok(()), Err(e) => { - error!("Failed to send forget me request: {}", e); + error!("Failed to send forget me request: {e}"); Err(Error::MessageSendingFailure) } } @@ -268,7 +268,7 @@ impl MixnetClient { match self.client_request_sender.send(client_request).await { Ok(_) => Ok(()), Err(e) => { - error!("Failed to send forget me request: {}", e); + error!("Failed to send forget me request: {e}"); Err(Error::MessageSendingFailure) } } diff --git a/service-providers/authenticator/src/cli/peer_handler.rs b/service-providers/authenticator/src/cli/peer_handler.rs index ebf34dd3b0..402c09815a 100644 --- a/service-providers/authenticator/src/cli/peer_handler.rs +++ b/service-providers/authenticator/src/cli/peer_handler.rs @@ -28,23 +28,23 @@ impl DummyHandler { if let Some(msg) = msg { match msg { PeerControlRequest::AddPeer { peer, client_id, response_tx } => { - log::info!("[DUMMY] Adding peer {:?} with client id {:?}", peer, client_id); + log::info!("[DUMMY] Adding peer {peer:?} with client id {client_id:?}"); response_tx.send(AddPeerControlResponse { success: true }).ok(); } PeerControlRequest::RemovePeer { key, response_tx } => { - log::info!("[DUMMY] Removing peer {:?}", key); + log::info!("[DUMMY] Removing peer {key:?}"); response_tx.send(RemovePeerControlResponse { success: true }).ok(); } PeerControlRequest::QueryPeer{key, response_tx} => { - log::info!("[DUMMY] Querying peer {:?}", key); + log::info!("[DUMMY] Querying peer {key:?}"); response_tx.send(QueryPeerControlResponse { success: true, peer: None }).ok(); } PeerControlRequest::QueryBandwidth{key, response_tx} => { - log::info!("[DUMMY] Querying bandwidth for peer {:?}", key); + log::info!("[DUMMY] Querying bandwidth for peer {key:?}"); response_tx.send(QueryBandwidthControlResponse { success: true, bandwidth_data: None }).ok(); } PeerControlRequest::GetClientBandwidth{key, response_tx} => { - log::info!("[DUMMY] Getting client bandwidth for peer {:?}", key); + log::info!("[DUMMY] Getting client bandwidth for peer {key:?}"); response_tx.send(GetClientBandwidthControlResponse {client_bandwidth: None }).ok(); } } diff --git a/service-providers/authenticator/src/cli/run.rs b/service-providers/authenticator/src/cli/run.rs index 67dc226523..d6b273f5b1 100644 --- a/service-providers/authenticator/src/cli/run.rs +++ b/service-providers/authenticator/src/cli/run.rs @@ -33,7 +33,7 @@ impl From<Run> for OverrideConfig { pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); + log::debug!("Using config: {config:#?}"); log::info!("Starting authenticator service provider"); let (wireguard_gateway_data, peer_rx) = WireguardGatewayData::new( diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 382c0f2900..b28df22982 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -866,7 +866,7 @@ impl MixnetListener { } pub(crate) async fn run(mut self) -> Result<()> { - log::info!("Using authenticator version {}", CURRENT_VERSION); + log::info!("Using authenticator version {CURRENT_VERSION}"); let mut task_client = self.task_handle.fork("main_loop"); while !task_client.is_shutdown() { @@ -876,7 +876,7 @@ impl MixnetListener { }, _ = self.timeout_check_interval.next() => { if let Err(e) = self.remove_stale_registrations().await { - log::error!("Could not clear stale registrations. The registration process might get jammed soon - {:?}", e); + log::error!("Could not clear stale registrations. The registration process might get jammed soon - {e:?}"); } self.seen_credential_cache.remove_stale(); } diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index 3e4010cb32..bf71e79dea 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -24,7 +24,7 @@ impl From<Run> for OverrideConfig { pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); + log::debug!("Using config: {config:#?}"); log::info!("Starting ip packet router service provider"); let mut server = nym_ip_packet_router::IpPacketRouter::new(config); diff --git a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs index 23360a2edd..551518a062 100644 --- a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs @@ -69,8 +69,8 @@ impl ConnectedClientHandler { oneshot::Sender<()>, tokio::task::JoinHandle<()>, ) { - log::debug!("Starting connected client handler for: {}", client_id); - log::debug!("client version: {:?}", client_version); + log::debug!("Starting connected client handler for: {client_id}"); + log::debug!("client version: {client_version:?}"); let (close_tx, close_rx) = oneshot::channel(); let (forward_from_tun_tx, forward_from_tun_rx) = mpsc::unbounded_channel(); diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 99b87075c8..1d0b5a3413 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -30,8 +30,7 @@ impl TryFrom<VersionedResponse> for IpPacketResponseV8 { match response.response { Response::StaticConnect { .. } => { return Err(IpPacketRouterError::UnsupportedResponse(format!( - "Static connect response is not supported in version {}", - version + "Static connect response is not supported in version {version}" ))) } Response::DynamicConnect { request_id, reply } => IpPacketResponseDataV8::Control( diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 3a35dc7ff0..fe622f8aa0 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -310,7 +310,7 @@ impl MixnetListener { // Check if the client is connected if !self.connected_clients.is_client_connected(&client_id) { - log::info!("Client {} is not connected, cannot disconnect", client_id); + log::info!("Client {client_id} is not connected, cannot disconnect"); return Ok(Some(VersionedResponse { version, reply_to: client_id, @@ -322,7 +322,7 @@ impl MixnetListener { } // Disconnect the client - log::info!("Disconnecting client {}", client_id); + log::info!("Disconnecting client {client_id}"); self.connected_clients.disconnect_client(&client_id); Ok(Some(VersionedResponse { diff --git a/service-providers/network-requester/examples/query.rs b/service-providers/network-requester/examples/query.rs index 87f410ee75..a11eeff143 100644 --- a/service-providers/network-requester/examples/query.rs +++ b/service-providers/network-requester/examples/query.rs @@ -13,7 +13,7 @@ fn parse_response(received: Vec<ReconstructedMessage>) -> Socks5Response { assert_eq!(received.len(), 1); let response: Response<Socks5Request> = Response::try_from_bytes(&received[0].message).unwrap(); match response.content { - ResponseContent::Control(control) => panic!("unexpected control response: {:?}", control), + ResponseContent::Control(control) => panic!("unexpected control response: {control:?}"), ResponseContent::ProviderData(data) => data, } } diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 6a061dd711..09938ac46a 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -47,7 +47,7 @@ impl From<Run> for OverrideConfig { pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); + log::debug!("Using config: {config:#?}"); if config.network_requester.open_proxy { println!( diff --git a/tools/echo-server/src/lib.rs b/tools/echo-server/src/lib.rs index 0bea93869a..4630e979dd 100644 --- a/tools/echo-server/src/lib.rs +++ b/tools/echo-server/src/lib.rs @@ -61,7 +61,7 @@ impl NymEchoServer { let home_dir = dirs::home_dir().expect("Unable to get home directory"); let default_path = format!("{}/tmp/nym-proxy-server-config", home_dir.display()); let config_path = config_path.unwrap_or(&default_path); - let listen_addr = format!("127.0.0.1:{}", listen_port); + let listen_addr = format!("127.0.0.1:{listen_port}"); let client = Arc::new(Mutex::new( tcp_proxy::NymProxyServer::new(&listen_addr, config_path, env, gateway).await?, @@ -337,7 +337,7 @@ mod tests { ); let coded_message = bincode::serialize(&outgoing)?; - println!("sending {:?}", coded_message); + println!("sending {coded_message:?}"); let mut client = MixnetClient::connect_new().await?; diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs index cdd97f9505..7e969e1e08 100644 --- a/tools/internal/testnet-manager/build.rs +++ b/tools/internal/testnet-manager/build.rs @@ -4,9 +4,9 @@ use std::env; #[tokio::main] async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/nym-api-example.sqlite", out_dir); + let database_path = format!("{out_dir}/nym-api-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await .expect("Failed to create SQLx database connection"); diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 9fa5a3d9dd..b8315de641 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -138,10 +138,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { async fn wait_for_interrupt() { if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); + error!("There was an error while capturing SIGINT - {e:?}. We will terminate regardless",); } println!( "Received SIGINT - the process will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." diff --git a/tools/nym-nr-query/src/main.rs b/tools/nym-nr-query/src/main.rs index 67980d638f..aecf7110d0 100644 --- a/tools/nym-nr-query/src/main.rs +++ b/tools/nym-nr-query/src/main.rs @@ -75,7 +75,7 @@ fn parse_socks5_response(received: Vec<mixnet::ReconstructedMessage>) -> Socks5R assert_eq!(received.len(), 1); let response: Response<Socks5Request> = Response::try_from_bytes(&received[0].message).unwrap(); match response.content { - ResponseContent::Control(control) => panic!("unexpected control response: {:?}", control), + ResponseContent::Control(control) => panic!("unexpected control response: {control:?}"), ResponseContent::ProviderData(data) => data, } } @@ -276,9 +276,9 @@ enum ClientResponse { impl fmt::Display for ClientResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ClientResponse::Control(control) => write!(f, "{:#?}", control), - ClientResponse::Query(query) => write!(f, "{:#?}", query), - ClientResponse::Ping(ping) => write!(f, "{}", ping), + ClientResponse::Control(control) => write!(f, "{control:#?}"), + ClientResponse::Query(query) => write!(f, "{query:#?}"), + ClientResponse::Ping(ping) => write!(f, "{ping}"), } } } diff --git a/wasm/client/src/encoded_payload_helper.rs b/wasm/client/src/encoded_payload_helper.rs index 71600ed5ce..8f2099425f 100644 --- a/wasm/client/src/encoded_payload_helper.rs +++ b/wasm/client/src/encoded_payload_helper.rs @@ -61,7 +61,7 @@ pub fn encode_payload_with_headers( Ok([size, metadata, payload].concat()) } Err(e) => Err(JsValue::from(JsError::new( - format!("Could not encode message: {}", e).as_str(), + format!("Could not encode message: {e}").as_str(), ))), } } @@ -84,7 +84,7 @@ pub fn decode_payload(message: Vec<u8>) -> Result<IEncodedPayload, JsValue> { .unwrap() .unchecked_into::<IEncodedPayload>()), Err(e) => Err(JsValue::from(JsError::new( - format!("Could not parse message: {}", e).as_str(), + format!("Could not parse message: {e}").as_str(), ))), } } From d0692a567adfec3cba0fc1153053972eab0e3d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Tue, 1 Jul 2025 11:29:50 +0100 Subject: [PATCH 41/47] feat: basic performance contract integration [within Nym API] (#5871) * renamed nym-api config fields * decouple rewarder startup from network monitor * additional sections in nym-api config * removed vesting queries in circulating supply calculator * added memoized field for last submitted performance measurement * wip: performance contract refresher * cleaned up various contract caches * modified cache refresher to allow passing update fn * implement performance cache refreshing * updated lefthook.yml to run cargo fmt * impl NodePerformanceProvider trait * dynamically using specific performance provider * pre warm up performance contract cache and forbid the mode if its empty * clippy * introduce fallback setting for performance contract if value for given epoch is not available * move some functions around --- .../performance_query_client.rs | 26 +- .../mixnet-contract/src/rewarding/mod.rs | 11 + .../nym-performance-contract/src/constants.rs | 1 + .../nym-performance-contract/src/msg.rs | 10 +- .../nym-performance-contract/src/types.rs | 18 +- .../schema/nym-performance-contract.json | 1240 +++++++++++++++++ contracts/performance/schema/raw/execute.json | 163 +++ .../performance/schema/raw/instantiate.json | 21 + contracts/performance/schema/raw/migrate.json | 6 + contracts/performance/schema/raw/query.json | 339 +++++ .../schema/raw/response_to_admin.json | 15 + .../response_to_epoch_measurements_paged.json | 69 + .../response_to_epoch_performance_paged.json | 63 + ..._to_full_historical_performance_paged.json | 75 + ...esponse_to_last_submitted_measurement.json | 100 ++ .../raw/response_to_network_monitor.json | 82 ++ .../response_to_network_monitors_paged.json | 87 ++ .../raw/response_to_node_measurements.json | 38 + .../raw/response_to_node_performance.json | 32 + .../response_to_node_performance_paged.json | 69 + ...nse_to_retired_network_monitors_paged.json | 73 + contracts/performance/src/contract.rs | 7 +- contracts/performance/src/queries.rs | 87 +- contracts/performance/src/storage.rs | 590 +++++++- contracts/performance/src/testing/mod.rs | 2 + contracts/performance/src/transactions.rs | 11 +- lefthook.yml | 4 + nym-api/nym-api-requests/src/models.rs | 4 + .../src/circulating_supply_api/cache/data.rs | 38 - .../src/circulating_supply_api/cache/mod.rs | 88 -- .../circulating_supply_api/cache/refresher.rs | 135 -- .../src/circulating_supply_api/handlers.rs | 29 +- nym-api/src/circulating_supply_api/mod.rs | 23 - nym-api/src/ecash/tests/mod.rs | 8 +- nym-api/src/epoch_operations/mod.rs | 8 +- nym-api/src/key_rotation/mod.rs | 6 +- nym-api/src/main.rs | 8 +- .../cache/data.rs | 33 +- .../cache/mod.rs | 55 +- .../cache/refresher.rs | 91 +- .../handlers.rs | 0 .../mod.rs | 16 +- nym-api/src/network/handlers.rs | 38 +- nym-api/src/network_monitor/mod.rs | 12 +- .../src/network_monitor/monitor/preparer.rs | 6 +- nym-api/src/node_describe_cache/provider.rs | 40 +- .../node_performance/contract_cache/data.rs | 88 ++ .../node_performance/contract_cache/mod.rs | 51 + .../contract_cache/refresher.rs | 135 ++ nym-api/src/node_performance/mod.rs | 5 + .../provider/contract_provider.rs | 94 ++ .../provider/legacy_storage_provider.rs | 91 ++ nym-api/src/node_performance/provider/mod.rs | 123 ++ .../src/node_status_api/cache/config_score.rs | 63 + nym-api/src/node_status_api/cache/mod.rs | 6 +- .../src/node_status_api/cache/node_sets.rs | 385 ----- .../src/node_status_api/cache/refresher.rs | 293 +++- nym-api/src/node_status_api/helpers.rs | 94 +- nym-api/src/node_status_api/mod.rs | 12 +- .../src/node_status_api/reward_estimate.rs | 90 -- nym-api/src/support/caching/cache.rs | 57 +- nym-api/src/support/caching/refresher.rs | 115 +- nym-api/src/support/cli/run.rs | 106 +- nym-api/src/support/config/mod.rs | 187 ++- nym-api/src/support/http/router.rs | 2 +- .../support/http/state/contract_details.rs | 182 +++ nym-api/src/support/http/state/mod.rs | 56 +- nym-api/src/support/nyxd/mod.rs | 80 +- nym-api/src/support/storage/manager.rs | 71 - nym-api/src/support/storage/mod.rs | 120 +- .../src/unstable_routes/v1/account/cache.rs | 6 +- .../v1/account/data_collector.rs | 6 +- 72 files changed, 4849 insertions(+), 1546 deletions(-) create mode 100644 contracts/performance/schema/nym-performance-contract.json create mode 100644 contracts/performance/schema/raw/execute.json create mode 100644 contracts/performance/schema/raw/instantiate.json create mode 100644 contracts/performance/schema/raw/migrate.json create mode 100644 contracts/performance/schema/raw/query.json create mode 100644 contracts/performance/schema/raw/response_to_admin.json create mode 100644 contracts/performance/schema/raw/response_to_epoch_measurements_paged.json create mode 100644 contracts/performance/schema/raw/response_to_epoch_performance_paged.json create mode 100644 contracts/performance/schema/raw/response_to_full_historical_performance_paged.json create mode 100644 contracts/performance/schema/raw/response_to_last_submitted_measurement.json create mode 100644 contracts/performance/schema/raw/response_to_network_monitor.json create mode 100644 contracts/performance/schema/raw/response_to_network_monitors_paged.json create mode 100644 contracts/performance/schema/raw/response_to_node_measurements.json create mode 100644 contracts/performance/schema/raw/response_to_node_performance.json create mode 100644 contracts/performance/schema/raw/response_to_node_performance_paged.json create mode 100644 contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json delete mode 100644 nym-api/src/circulating_supply_api/cache/data.rs delete mode 100644 nym-api/src/circulating_supply_api/cache/mod.rs delete mode 100644 nym-api/src/circulating_supply_api/cache/refresher.rs rename nym-api/src/{nym_contract_cache => mixnet_contract_cache}/cache/data.rs (63%) rename nym-api/src/{nym_contract_cache => mixnet_contract_cache}/cache/mod.rs (78%) rename nym-api/src/{nym_contract_cache => mixnet_contract_cache}/cache/refresher.rs (59%) rename nym-api/src/{nym_contract_cache => mixnet_contract_cache}/handlers.rs (100%) rename nym-api/src/{nym_contract_cache => mixnet_contract_cache}/mod.rs (50%) create mode 100644 nym-api/src/node_performance/contract_cache/data.rs create mode 100644 nym-api/src/node_performance/contract_cache/mod.rs create mode 100644 nym-api/src/node_performance/contract_cache/refresher.rs create mode 100644 nym-api/src/node_performance/mod.rs create mode 100644 nym-api/src/node_performance/provider/contract_provider.rs create mode 100644 nym-api/src/node_performance/provider/legacy_storage_provider.rs create mode 100644 nym-api/src/node_performance/provider/mod.rs create mode 100644 nym-api/src/node_status_api/cache/config_score.rs delete mode 100644 nym-api/src/node_status_api/cache/node_sets.rs delete mode 100644 nym-api/src/node_status_api/reward_estimate.rs create mode 100644 nym-api/src/support/http/state/contract_details.rs diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs index 722a2d002c..470a412e8d 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs @@ -7,18 +7,17 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; -pub use nym_performance_contract_common::{ - msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, -}; -use nym_performance_contract_common::{ - EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, - NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse, - NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor, - RetiredNetworkMonitorsPagedResponse, -}; use serde::Deserialize; +pub use nym_performance_contract_common::{ + msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, EpochId, + EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, + NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, +}; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait PerformanceQueryClient { @@ -139,6 +138,11 @@ pub trait PerformanceQueryClient { }) .await } + + async fn get_last_submission(&self) -> Result<LastSubmission, NyxdError> { + self.query_performance_contract(PerformanceQueryMsg::LastSubmittedMeasurement {}) + .await + } } // extension trait to the query client to deal with the paged queries @@ -212,6 +216,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -260,6 +265,7 @@ mod tests { PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client .get_retired_network_monitors_paged(start_after, limit) .ignore(), + QueryMsg::LastSubmittedMeasurement {} => client.get_last_submission().ignore(), }; } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index ce54caab33..374b30bf0c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -44,6 +44,17 @@ pub struct RewardEstimate { pub operating_cost: Decimal, } +impl RewardEstimate { + pub const fn zero() -> RewardEstimate { + RewardEstimate { + total_node_reward: Decimal::zero(), + operator: Decimal::zero(), + delegates: Decimal::zero(), + operating_cost: Decimal::zero(), + } + } +} + #[cw_serde] #[derive(Copy, Default)] pub struct RewardDistribution { diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs index b452f95916..5f98a8e439 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -4,6 +4,7 @@ pub mod storage_keys { pub const CONTRACT_ADMIN: &str = "contract-admin"; pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id"; + pub const LAST_SUBMISSION: &str = "last-submission"; pub const MIXNET_CONTRACT: &str = "mixnet-contract"; pub const AUTHORISED_COUNT: &str = "authorised-count"; pub const AUTHORISED: &str = "authorised"; diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs index cf03b650b7..74f512d891 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -7,9 +7,9 @@ use cosmwasm_schema::cw_serde; #[cfg(feature = "schema")] use crate::types::{ EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse, - NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse, - RetiredNetworkMonitorsPagedResponse, + FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, }; #[cw_serde] @@ -113,6 +113,10 @@ pub enum QueryMsg { start_after: Option<String>, limit: Option<u32>, }, + + /// Returns information regarding the latest submitted performance data + #[cfg_attr(feature = "schema", returns(LastSubmission))] + LastSubmittedMeasurement {}, } #[cw_serde] diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs index 10ef055d28..383fb08280 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -2,12 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Env}; +use cosmwasm_std::{Addr, Env, Timestamp}; use nym_contracts_common::Percent; pub type EpochId = u32; pub type NodeId = u32; +#[cw_serde] +pub struct LastSubmission { + pub block_height: u64, + pub block_time: Timestamp, + + // not as relevant, but might as well store it + pub data: Option<LastSubmittedData>, +} + +#[cw_serde] +pub struct LastSubmittedData { + pub sender: Addr, + pub epoch_id: EpochId, + pub data: NodePerformance, +} + #[cw_serde] pub struct NetworkMonitorDetails { pub address: Addr, diff --git a/contracts/performance/schema/nym-performance-contract.json b/contracts/performance/schema/nym-performance-contract.json new file mode 100644 index 0000000000..2242aefd75 --- /dev/null +++ b/contracts/performance/schema/nym-performance-contract.json @@ -0,0 +1,1240 @@ +{ + "contract_name": "nym-performance-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "epoch_measurements_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "epoch_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "full_historical_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "last_submitted_measurement": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "network_monitor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "node_measurements": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "retired_network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/performance/schema/raw/execute.json b/contracts/performance/schema/raw/execute.json new file mode 100644 index 0000000000..c4b5c8be04 --- /dev/null +++ b/contracts/performance/schema/raw/execute.json @@ -0,0 +1,163 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/instantiate.json b/contracts/performance/schema/raw/instantiate.json new file mode 100644 index 0000000000..4380993e57 --- /dev/null +++ b/contracts/performance/schema/raw/instantiate.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/migrate.json b/contracts/performance/schema/raw/migrate.json new file mode 100644 index 0000000000..7fbe8c5708 --- /dev/null +++ b/contracts/performance/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/query.json b/contracts/performance/schema/raw/query.json new file mode 100644 index 0000000000..bcca411780 --- /dev/null +++ b/contracts/performance/schema/raw/query.json @@ -0,0 +1,339 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/performance/schema/raw/response_to_admin.json b/contracts/performance/schema/raw/response_to_admin.json new file mode 100644 index 0000000000..c73969ab04 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json new file mode 100644 index 0000000000..4c16b66bb7 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_epoch_performance_paged.json b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json new file mode 100644 index 0000000000..235c12b64f --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json new file mode 100644 index 0000000000..6f0eb6e191 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_last_submitted_measurement.json b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json new file mode 100644 index 0000000000..7c3779d9a6 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitor.json b/contracts/performance/schema/raw/response_to_network_monitor.json new file mode 100644 index 0000000000..eecd26abe3 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitor.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_network_monitors_paged.json new file mode 100644 index 0000000000..a811963072 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitors_paged.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_measurements.json b/contracts/performance/schema/raw/response_to_node_measurements.json new file mode 100644 index 0000000000..e1add3c02d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_measurements.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance.json b/contracts/performance/schema/raw/response_to_node_performance.json new file mode 100644 index 0000000000..d9bae2e00d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance_paged.json b/contracts/performance/schema/raw/response_to_node_performance_paged.json new file mode 100644 index 0000000000..f63c28f477 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json new file mode 100644 index 0000000000..f5310e9438 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs index 235cef391f..2f488f3961 100644 --- a/contracts/performance/src/contract.rs +++ b/contracts/performance/src/contract.rs @@ -3,7 +3,7 @@ use crate::queries::{ query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, - query_full_historical_performance_paged, query_network_monitor_details, + query_full_historical_performance_paged, query_last_submission, query_network_monitor_details, query_network_monitors_paged, query_node_measurements, query_node_performance, query_node_performance_paged, query_retired_network_monitors_paged, }; @@ -57,10 +57,10 @@ pub fn execute( match msg { ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), ExecuteMsg::Submit { epoch, data } => { - try_submit_performance_results(deps, info, epoch, data) + try_submit_performance_results(deps, env, info, epoch, data) } ExecuteMsg::BatchSubmit { epoch, data } => { - try_batch_submit_performance_results(deps, info, epoch, data) + try_batch_submit_performance_results(deps, env, info, epoch, data) } ExecuteMsg::AuthoriseNetworkMonitor { address } => { try_authorise_network_monitor(deps, env, info, address) @@ -129,6 +129,7 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NymPerformance start_after, limit, )?)?), + QueryMsg::LastSubmittedMeasurement {} => Ok(to_json_binary(&query_last_submission(deps)?)?), } } diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs index 3cc095481a..5fe2c3ed91 100644 --- a/contracts/performance/src/queries.rs +++ b/contracts/performance/src/queries.rs @@ -7,9 +7,9 @@ use cw_controllers::AdminResponse; use cw_storage_plus::Bound; use nym_performance_contract_common::{ EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, - NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, - NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, + NodeMeasurement, NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, }; @@ -305,11 +305,19 @@ pub fn query_retired_network_monitors_paged( }) } +pub fn query_last_submission(deps: Deps) -> Result<LastSubmission, NymPerformanceContractError> { + NYM_PERFORMANCE_CONTRACT_STORAGE + .last_performance_submission + .load(deps.storage) + .map_err(Into::into) +} + #[cfg(test)] mod tests { use super::*; use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; - use nym_contracts_common_testing::{ContractOpts, RandExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::LastSubmittedData; #[cfg(test)] mod admin_query { @@ -585,4 +593,75 @@ mod tests { Ok(()) } + + #[test] + fn last_submission_query() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + + let id1 = test.bond_dummy_nymnode()?; + let id2 = test.bond_dummy_nymnode()?; + + // initial + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + } + ); + + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + test.set_mixnet_epoch(10)?; + + test.insert_raw_performance(&nm1, id1, "0.2")?; + + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm1.clone(), + epoch_id: 10, + data: NodePerformance { + node_id: id1, + performance: "0.2".parse()? + }, + }), + } + ); + + test.next_block(); + let env = test.env(); + + test.insert_epoch_performance(&nm2, 5, id2, "0.3".parse()?)?; + + // note that even though it's "earlier" data, last submission is still updated accordingly + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm2.clone(), + epoch_id: 5, + data: NodePerformance { + node_id: id2, + performance: "0.3".parse()? + }, + }), + } + ); + + Ok(()) + } } diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index 0559352a28..acba90762b 100644 --- a/contracts/performance/src/storage.rs +++ b/contracts/performance/src/storage.rs @@ -8,9 +8,9 @@ use cw_storage_plus::{Item, Map}; use nym_contracts_common::Percent; use nym_performance_contract_common::constants::storage_keys; use nym_performance_contract_common::{ - BatchSubmissionResult, EpochId, NetworkMonitorDetails, NetworkMonitorSubmissionMetadata, - NodeId, NodePerformance, NodeResults, NymPerformanceContractError, - RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, + BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails, + NetworkMonitorSubmissionMetadata, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, }; pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = @@ -19,6 +19,7 @@ pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = pub struct NymPerformanceContractStorage { pub(crate) contract_admin: Admin, pub(crate) mixnet_epoch_id_at_creation: Item<EpochId>, + pub(crate) last_performance_submission: Item<LastSubmission>, pub(crate) mixnet_contract_address: Item<Addr>, @@ -33,6 +34,7 @@ impl NymPerformanceContractStorage { NymPerformanceContractStorage { contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), mixnet_epoch_id_at_creation: Item::new(storage_keys::INITIAL_EPOCH_ID), + last_performance_submission: Item::new(storage_keys::LAST_SUBMISSION), mixnet_contract_address: Item::new(storage_keys::MIXNET_CONTRACT), network_monitors: NetworkMonitorsStorage::new(), performance_results: PerformanceResultsStorage::new(), @@ -77,6 +79,16 @@ impl NymPerformanceContractStorage { let initial_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + // set the last submission to the initial value + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }, + )?; + // set the initial epoch id self.mixnet_epoch_id_at_creation .save(deps.storage, &initial_epoch_id)?; @@ -100,6 +112,7 @@ impl NymPerformanceContractStorage { pub fn submit_performance_data( &self, deps: DepsMut, + env: Env, sender: &Addr, epoch_id: EpochId, data: NodePerformance, @@ -135,12 +148,27 @@ impl NymPerformanceContractStorage { data.node_id, )?; + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data, + }), + }, + )?; + Ok(()) } pub fn batch_submit_performance_results( &self, deps: DepsMut, + env: Env, sender: &Addr, epoch_id: EpochId, data: Vec<NodePerformance>, @@ -208,6 +236,20 @@ impl NymPerformanceContractStorage { last.node_id, )?; + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data: *last, + }), + }, + )?; + Ok(BatchSubmissionResult { accepted_scores, non_existent_nodes, @@ -592,6 +634,25 @@ mod tests { Ok(()) } + #[test] + fn sets_initial_submission_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let env = pre_init.env(); + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + + let expected = LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }; + let data = storage.last_performance_submission.load(deps.storage)?; + assert_eq!(expected, data); + Ok(()) + } + #[test] fn retrieves_initial_epoch_id_from_mixnet_contract() -> anyhow::Result<()> { // base case @@ -721,18 +782,19 @@ mod tests { let nm1 = tester.addr_make("network-monitor-1"); let nm2 = tester.addr_make("network-monitor-2"); let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); tester.authorise_network_monitor(&nm1)?; // authorised network monitor can submit the results just fine let perf = tester.dummy_node_performance(); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm1, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm1, 0, perf) .is_ok()); // unauthorised address is rejected let res = storage - .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) .unwrap_err(); assert_eq!( res, @@ -744,12 +806,12 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; assert!(storage - .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) .is_ok()); // and address that was never authorised still fails let res = storage - .submit_performance_data(tester.deps_mut(), &unauthorised, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &unauthorised, 0, perf) .unwrap_err(); assert_eq!( res, @@ -764,6 +826,7 @@ mod tests { fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -781,12 +844,12 @@ mod tests { // first submission assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .is_ok()); // second submission let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -801,16 +864,16 @@ mod tests { // another submission works fine assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) .is_ok()); // original one works IF it's for next epoch assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 1, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 1, data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -832,6 +895,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -845,11 +909,11 @@ mod tests { }; assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -864,11 +928,11 @@ mod tests { // check across epochs assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 10, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 9, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, data) .unwrap_err(); assert_eq!( @@ -891,11 +955,12 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); // if NM got authorised at epoch 10, it can only submit data for epochs >=10 let perf = tester.dummy_node_performance(); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf) .unwrap_err(); assert_eq!( @@ -909,7 +974,7 @@ mod tests { ); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 9, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf) .unwrap_err(); assert_eq!( @@ -923,10 +988,10 @@ mod tests { ); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 10, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, perf) .is_ok()); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 11, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 11, perf) .is_ok()); Ok(()) @@ -936,6 +1001,7 @@ mod tests { fn updates_submission_metadata() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let mut nodes = Vec::new(); for _ in 0..10 { @@ -953,6 +1019,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 0, NodePerformance { @@ -969,6 +1036,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 0, NodePerformance { @@ -985,6 +1053,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 1, NodePerformance { @@ -1001,6 +1070,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 12345, NodePerformance { @@ -1018,10 +1088,136 @@ mod tests { Ok(()) } + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 12345, + data: NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + #[test] fn requires_associated_node_to_be_bonded() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1033,7 +1229,7 @@ mod tests { // no node bonded at this point let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1048,14 +1244,15 @@ mod tests { node_id, performance: Default::default(), }; - let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); assert!(res.is_ok()); // unbonded tester.unbond_nymnode(node_id)?; let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1070,14 +1267,15 @@ mod tests { node_id, performance: Default::default(), }; - let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); assert!(res.is_ok()); // unbonded tester.unbond_legacy_mixnode(node_id)?; let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1100,18 +1298,31 @@ mod tests { let nm1 = tester.addr_make("network-monitor-1"); let nm2 = tester.addr_make("network-monitor-2"); let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); tester.authorise_network_monitor(&nm1)?; let perf = tester.dummy_node_performance(); // authorised network monitor can submit the results just fine assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm1, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + vec![perf] + ) .is_ok()); // unauthorised address is rejected let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf], + ) .unwrap_err(); assert_eq!( res, @@ -1123,13 +1334,20 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf] + ) .is_ok()); // and address that was never authorised still fails let res = storage .batch_submit_performance_results( tester.deps_mut(), + env.clone(), &unauthorised, 0, vec![perf], @@ -1150,6 +1368,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -1174,27 +1393,57 @@ mod tests { let sorted = vec![data, another_data, more_data]; let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, duplicates) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + duplicates, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, another_dups) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_dups, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, unsorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + unsorted, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, semi_sorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + semi_sorted, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, sorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + sorted + ) .is_ok()); Ok(()) } @@ -1205,6 +1454,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -1219,12 +1469,24 @@ mod tests { // first submission assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data] + ) .is_ok()); // second submission let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1239,16 +1501,34 @@ mod tests { // another submission works fine assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) .is_ok()); // original one works IF it's for next epoch assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 1, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1268,6 +1548,7 @@ mod tests { fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1283,11 +1564,23 @@ mod tests { }; assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1302,11 +1595,23 @@ mod tests { // check across epochs assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1325,8 +1630,9 @@ mod tests { fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); - tester.set_mixnet_epoch(10)?; + let env = tester.env(); + tester.set_mixnet_epoch(10)?; let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1334,7 +1640,13 @@ mod tests { // if NM got authorised at epoch 10, it can only submit data for epochs >=10 let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![perf], + ) .unwrap_err(); assert_eq!( @@ -1348,7 +1660,13 @@ mod tests { ); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![perf], + ) .unwrap_err(); assert_eq!( @@ -1362,10 +1680,22 @@ mod tests { ); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![perf] + ) .is_ok()); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 11, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 11, + vec![perf] + ) .is_ok()); Ok(()) @@ -1375,6 +1705,7 @@ mod tests { fn updates_submission_metadata() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1393,6 +1724,7 @@ mod tests { // single submission storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 0, vec![NodePerformance { @@ -1410,6 +1742,7 @@ mod tests { // another epoch storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![NodePerformance { @@ -1427,6 +1760,7 @@ mod tests { // multiple submissions storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![ @@ -1454,6 +1788,7 @@ mod tests { // another epoch storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 2, vec![ @@ -1481,6 +1816,155 @@ mod tests { Ok(()) } + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + }), + } + ); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[7], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 2, + data: NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + #[test] fn informs_if_associated_node_is_not_bonded() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); @@ -1496,22 +1980,21 @@ mod tests { } let nym_node1 = tester.bond_dummy_nymnode()?; - let nym_node_between = tester.bond_dummy_nymnode()?; tester.unbond_nymnode(nym_node_between)?; - let nym_node2 = tester.bond_dummy_nymnode()?; let mix_node1 = tester.bond_dummy_legacy_mixnode()?; - let mixnode_between = tester.bond_dummy_legacy_mixnode()?; tester.unbond_legacy_mixnode(mixnode_between)?; - let mix_node2 = tester.bond_dummy_legacy_mixnode()?; + let env = tester.env(); + // single id - nothing bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 0, vec![NodePerformance { @@ -1525,6 +2008,7 @@ mod tests { // one bonded nym-node, one not bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![ @@ -1544,6 +2028,7 @@ mod tests { // not-bonded, bonded, not-bonded, bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 2, vec![ @@ -1573,6 +2058,7 @@ mod tests { // one bonded mixnode, one not bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 3, vec![ @@ -1592,6 +2078,7 @@ mod tests { // not-bonded, bonded, not-bonded, bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 4, vec![ @@ -1619,6 +2106,7 @@ mod tests { // nym-node, not bonded, mixnode let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 5, vec![ diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index c1295d38a5..4f8e7cf4bd 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -386,8 +386,10 @@ pub(crate) trait PerformanceContractTesterExt: node_id: NodeId, performance: Percent, ) -> Result<(), NymPerformanceContractError> { + let env = self.env(); NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( self.deps_mut(), + env, addr, epoch_id, NodePerformance { diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs index 6385661fa3..86a2d99772 100644 --- a/contracts/performance/src/transactions.rs +++ b/contracts/performance/src/transactions.rs @@ -23,11 +23,18 @@ pub fn try_update_contract_admin( pub fn try_submit_performance_results( deps: DepsMut<'_>, + env: Env, info: MessageInfo, epoch_id: EpochId, data: NodePerformance, ) -> Result<Response, NymPerformanceContractError> { - NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(deps, &info.sender, epoch_id, data)?; + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + deps, + env, + &info.sender, + epoch_id, + data, + )?; // TODO: emit events Ok(Response::new()) @@ -35,12 +42,14 @@ pub fn try_submit_performance_results( pub fn try_batch_submit_performance_results( deps: DepsMut<'_>, + env: Env, info: MessageInfo, epoch_id: EpochId, data: Vec<NodePerformance>, ) -> Result<Response, NymPerformanceContractError> { let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( deps, + env, &info.sender, epoch_id, data, diff --git a/lefthook.yml b/lefthook.yml index 11b130f92a..0687e78972 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -47,3 +47,7 @@ pre-commit: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,css}" run: yarn biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true + rust-lint: + glob: "*.rs" + run: cargo fmt + stage_fixed: true diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a9c5c2ccd4..3c23c4b106 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -267,6 +267,10 @@ impl RoutingScore { Self { score } } + pub const fn zero() -> RoutingScore { + RoutingScore { score: 0.0 } + } + pub fn legacy_performance(&self) -> Performance { Performance::naive_try_from_f64(self.score).unwrap_or_default() } diff --git a/nym-api/src/circulating_supply_api/cache/data.rs b/nym-api/src/circulating_supply_api/cache/data.rs deleted file mode 100644 index 77ddbaaee2..0000000000 --- a/nym-api/src/circulating_supply_api/cache/data.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::caching::Cache; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; - -pub(crate) struct CirculatingSupplyCacheData { - // no need to cache that one as it's constant, but let's put it here for consistency sake - pub(crate) total_supply: Coin, - pub(crate) mixmining_reserve: Cache<Coin>, - pub(crate) vesting_tokens: Cache<Coin>, - pub(crate) circulating_supply: Cache<Coin>, -} - -impl CirculatingSupplyCacheData { - pub fn new(mix_denom: String) -> CirculatingSupplyCacheData { - let zero_coin = Coin::new(0, &mix_denom); - - CirculatingSupplyCacheData { - total_supply: Coin::new(1_000_000_000_000_000, mix_denom), - mixmining_reserve: Cache::new(zero_coin.clone()), - vesting_tokens: Cache::new(zero_coin.clone()), - circulating_supply: Cache::new(zero_coin), - } - } -} - -impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse { - fn from(value: &'a CirculatingSupplyCacheData) -> Self { - CirculatingSupplyResponse { - total_supply: value.total_supply.clone().into(), - mixmining_reserve: value.mixmining_reserve.clone().into(), - vesting_tokens: value.vesting_tokens.clone().into(), - circulating_supply: value.circulating_supply.clone().into(), - } - } -} diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs deleted file mode 100644 index 87e401da9d..0000000000 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use self::data::CirculatingSupplyCacheData; -use cosmwasm_std::Addr; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::nyxd::Coin; -use std::ops::Deref; -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Duration, -}; -use thiserror::Error; -use tokio::sync::RwLock; -use tokio::time; -use tracing::{error, info}; - -mod data; -pub(crate) mod refresher; - -#[derive(Debug, Error)] -enum CirculatingSupplyCacheError { - // this can only happen if somebody decides to set their staking address - // before https://github.com/nymtech/nym/pull/2796 is deployed - #[error("vesting account owned by {owner} with id {account_id} appeared more than once in the query response")] - DuplicateVestingAccountEntry { owner: Addr, account_id: u32 }, - - // this can happen if somehow the query was incomplete, like some paged sub-query didn't return full result - // or there's a bug with paging. or if, somehow, a vesting account got removed from the contract - #[error("got an inconsistent number of vesting account. received data on {got}, but expected {expected}")] - InconsistentNumberOfVestingAccounts { expected: usize, got: usize }, - - #[error(transparent)] - ClientError { - #[from] - source: NyxdError, - }, -} - -/// A cache for the circulating supply of the network. Circulating supply is calculated by -/// taking the initial supply of 1bn coins, and subtracting the amount of coins that are -/// in the mixmining pool and tied up in vesting. -/// -/// The cache is quite simple and does not include an update listener that the other caches have. -#[derive(Clone)] -pub(crate) struct CirculatingSupplyCache { - initialised: Arc<AtomicBool>, - data: Arc<RwLock<CirculatingSupplyCacheData>>, -} - -impl CirculatingSupplyCache { - pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache { - CirculatingSupplyCache { - initialised: Arc::new(AtomicBool::new(false)), - data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))), - } - } - - pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { - match time::timeout(Duration::from_millis(100), self.data.read()).await { - Ok(cache) => Some(cache.deref().into()), - Err(err) => { - error!("Failed to get circulating supply: {err}"); - None - } - } - } - - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { - let mut cache = self.data.write().await; - - let mut circulating_supply = cache.total_supply.clone(); - circulating_supply.amount -= mixmining_reserve.amount; - circulating_supply.amount -= vesting_tokens.amount; - - info!("Updating circulating supply cache"); - info!("the mixmining reserve is now {mixmining_reserve}"); - info!("the number of tokens still vesting is now {vesting_tokens}"); - info!("the circulating supply is now {circulating_supply}"); - - cache.mixmining_reserve.unchecked_update(mixmining_reserve); - cache.vesting_tokens.unchecked_update(vesting_tokens); - cache - .circulating_supply - .unchecked_update(circulating_supply); - } -} diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs deleted file mode 100644 index 60d7c2bafc..0000000000 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use super::CirculatingSupplyCache; -use crate::circulating_supply_api::cache::CirculatingSupplyCacheError; -use crate::support::nyxd::Client; -use nym_contracts_common::truncate_decimal; -use nym_task::TaskClient; -use nym_validator_client::nyxd::Coin; -use std::collections::HashSet; -use std::sync::atomic::Ordering; -use std::time::Duration; -use tokio::time; -use tracing::{error, trace}; - -pub(crate) struct CirculatingSupplyCacheRefresher { - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, -} - -impl CirculatingSupplyCacheRefresher { - pub(crate) fn new( - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, - ) -> Self { - CirculatingSupplyCacheRefresher { - nyxd_client, - cache, - caching_interval, - } - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - let mut interval = time::interval(self.caching_interval); - while !shutdown.is_shutdown() { - tokio::select! { - _ = interval.tick() => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - ret = self.refresh() => { - if let Err(err) = ret { - error!("Failed to refresh circulating supply cache - {err}"); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) - } - } - } - } - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - } - } - } - - async fn get_mixmining_reserve( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let reward_pool = self - .nyxd_client - .get_current_rewarding_parameters() - .await? - .interval - .reward_pool; - - Ok(Coin::new(truncate_decimal(reward_pool).u128(), mix_denom)) - } - - async fn get_total_vesting_tokens( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let all_vesting = self.nyxd_client.get_all_vesting_coins().await?; - - // sanity check invariants to make sure all accounts got considered and we got no duplicates - // the cache refreshes so infrequently that the performance penalty is negligible - let mut owners = HashSet::new(); - let mut ids = HashSet::new(); - for acc in &all_vesting { - if !owners.insert(acc.owner.clone()) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - - if !ids.insert(acc.account_id) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - } - - let current_storage_key = self - .nyxd_client - .get_current_vesting_account_storage_key() - .await?; - if all_vesting.len() != current_storage_key as usize { - return Err( - CirculatingSupplyCacheError::InconsistentNumberOfVestingAccounts { - expected: current_storage_key as usize, - got: all_vesting.len(), - }, - ); - } - - let mut total = Coin::new(0, mix_denom); - for account in all_vesting { - total.amount += account.still_vesting.amount.u128(); - } - - Ok(total) - } - - async fn refresh(&self) -> Result<(), CirculatingSupplyCacheError> { - let chain_details = self.nyxd_client.chain_details().await; - let mix_denom = &chain_details.mix_denom.base; - - let mixmining_reserve = self.get_mixmining_reserve(mix_denom).await?; - let vesting_tokens = self.get_total_vesting_tokens(mix_denom).await?; - - self.cache.update(mixmining_reserve, vesting_tokens).await; - Ok(()) - } -} diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index afc04c0b12..3299afa7f1 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,10 +1,11 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::Query; -use axum::{extract, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::models::CirculatingSupplyResponse; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_validator_client::nyxd::Coin; @@ -34,15 +35,11 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> { )] async fn get_full_circulating_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<CirculatingSupplyResponse>> { let output = output.output.unwrap_or_default(); - match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + match contract_cache.get_circulating_supply().await { Some(value) => Ok(output.to_response(value)), None => Err(AxumErrorResponse::internal_msg("unavailable")), } @@ -63,14 +60,10 @@ async fn get_full_circulating_supply( )] async fn get_total_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<f64>> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; @@ -95,15 +88,11 @@ async fn get_total_supply( )] async fn get_circulating_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<f64>> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 3253ccaaab..d1a19b2932 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,27 +1,4 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use self::cache::refresher::CirculatingSupplyCacheRefresher; -use crate::support::{config, nyxd}; -use nym_task::TaskManager; - -pub(crate) mod cache; pub(crate) mod handlers; - -/// Spawn the circulating supply cache refresher. -pub(crate) fn start_cache_refresh( - config: &config::CirculatingSupplyCacher, - nyxd_client: nyxd::Client, - circulating_supply_cache: &cache::CirculatingSupplyCache, - shutdown: &TaskManager, -) { - if config.enabled { - let refresher = CirculatingSupplyCacheRefresher::new( - nyxd_client, - circulating_supply_cache.to_owned(), - config.debug.caching_interval, - ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { refresher.run(shutdown_listener).await }); - } -} diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 22ec524a29..faa142c2ea 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,20 +1,20 @@ // Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::nyxd::Client; @@ -1279,9 +1279,8 @@ impl TestFixture { chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), - nym_contract_cache: NymContractCache::new(), + mixnet_contract_cache: MixnetContractCache::new(), node_status_cache: NodeStatusCache::new(), - circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), storage, described_nodes_cache: SharedCache::<DescribedNodes>::new(), network_details: NetworkDetails::new( @@ -1289,6 +1288,7 @@ impl TestFixture { NymNetworkDetails::new_empty(), ), node_info_cache: unstable::NodeInfoCache::default(), + contract_info_cache: ContractDetailsCache::new(Duration::from_secs(42)), api_status: ApiStatusState::new(None), ecash_state: Arc::new(ecash_state), } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index dd2c64945e..572b941c99 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -12,9 +12,9 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::{NodeStatusCache, ONE_DAY}; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; @@ -37,7 +37,7 @@ mod transition_beginning; // this is struct responsible for advancing an epoch pub struct EpochAdvancer { nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, status_cache: NodeStatusCache, storage: NymApiStorage, @@ -52,7 +52,7 @@ impl EpochAdvancer { pub(crate) fn new( nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, status_cache: NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: NymApiStorage, @@ -239,7 +239,7 @@ impl EpochAdvancer { pub(crate) fn start( nyxd_client: Client, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, status_cache: &NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: &NymApiStorage, diff --git a/nym-api/src/key_rotation/mod.rs b/nym-api/src/key_rotation/mod.rs index 6d648c2e92..304e9ae328 100644 --- a/nym-api/src/key_rotation/mod.rs +++ b/nym-api/src/key_rotation/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester}; use nym_mixnet_contract_common::{Interval, KeyRotationState}; use nym_task::TaskClient; @@ -59,14 +59,14 @@ pub(crate) struct KeyRotationController { pub(crate) describe_cache_refresher: RefreshRequester, pub(crate) contract_cache_watcher: CacheUpdateWatcher, - pub(crate) contract_cache: NymContractCache, + pub(crate) contract_cache: MixnetContractCache, } impl KeyRotationController { pub(crate) fn new( describe_cache_refresher: RefreshRequester, contract_cache_watcher: CacheUpdateWatcher, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, ) -> KeyRotationController { KeyRotationController { last_described_refreshed_for: None, diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 76bd1ab3dd..c07a6c2e8f 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,17 +1,14 @@ // Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - use crate::epoch_operations::EpochAdvancer; use crate::support::cli; use crate::support::storage; use ::nym_config::defaults::setup_env; use clap::Parser; +use mixnet_contract_cache::cache::MixnetContractCache; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_tracing_logger; -use nym_contract_cache::cache::NymContractCache; use support::nyxd; use tracing::{info, trace}; @@ -19,11 +16,12 @@ mod circulating_supply_api; mod ecash; mod epoch_operations; mod key_rotation; +pub(crate) mod mixnet_contract_cache; pub(crate) mod network; mod network_monitor; pub(crate) mod node_describe_cache; +mod node_performance; pub(crate) mod node_status_api; -pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/mixnet_contract_cache/cache/data.rs similarity index 63% rename from nym-api/src/nym_contract_cache/cache/data.rs rename to nym-api/src/mixnet_contract_cache/cache/data.rs index f54f9c48f1..d648c1037b 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/mixnet_contract_cache/cache/data.rs @@ -3,14 +3,11 @@ use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::ConfigScoreDataResponse; -use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::{ ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; -use nym_validator_client::nyxd::AccountId; -use std::collections::HashMap; #[derive(Clone)] pub(crate) struct ConfigScoreData { @@ -31,7 +28,9 @@ impl From<ConfigScoreData> for ConfigScoreDataResponse { } } -pub(crate) struct ContractCacheData { +pub(crate) struct MixnetContractCacheData { + pub(crate) rewarding_denom: String, + pub(crate) legacy_mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, pub(crate) legacy_gateways: Vec<LegacyGatewayBondWithId>, pub(crate) nym_nodes: Vec<NymNodeDetails>, @@ -41,30 +40,4 @@ pub(crate) struct ContractCacheData { pub(crate) current_reward_params: RewardingParams, pub(crate) current_interval: Interval, pub(crate) key_rotation_state: KeyRotationState, - - pub(crate) contracts_info: CachedContractsInfo, -} - -type ContractAddress = String; -pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; - -#[derive(Clone)] -pub struct CachedContractInfo { - pub(crate) address: Option<AccountId>, - pub(crate) base: Option<cw2::ContractVersion>, - pub(crate) detailed: Option<ContractBuildInformation>, -} - -impl CachedContractInfo { - pub fn new( - address: Option<&AccountId>, - base: Option<cw2::ContractVersion>, - detailed: Option<ContractBuildInformation>, - ) -> Self { - Self { - address: address.cloned(), - base, - detailed, - } - } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/mixnet_contract_cache/cache/mod.rs similarity index 78% rename from nym-api/src/nym_contract_cache/cache/mod.rs rename to nym-api/src/mixnet_contract_cache/cache/mod.rs index e73dde0f41..39915845e8 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/cache/mod.rs @@ -1,52 +1,56 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::refresh::RefreshData; -use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::Cache; -use data::ContractCacheData; +use data::MixnetContractCacheData; use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use nym_api_requests::models::MixnodeStatus; +use nym_api_requests::models::{CirculatingSupplyResponse, MixnodeStatus}; +use nym_contracts_common::truncate_decimal; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ Interval, KeyRotationState, NodeId, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; +use nym_validator_client::nyxd::Coin; use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; pub(crate) mod data; pub(crate) mod refresher; +const TOTAL_SUPPLY_AMOUNT: u128 = 1_000_000_000_000_000; // 1B tokens + #[derive(Clone)] -pub struct NymContractCache { - pub(crate) inner: SharedCache<ContractCacheData>, +pub struct MixnetContractCache { + pub(crate) inner: SharedCache<MixnetContractCacheData>, } -impl NymContractCache { +impl MixnetContractCache { pub(crate) fn new() -> Self { - NymContractCache { + MixnetContractCache { inner: SharedCache::new(), } } - pub(crate) fn inner(&self) -> SharedCache<ContractCacheData> { + pub(crate) fn inner(&self) -> SharedCache<MixnetContractCacheData> { self.inner.clone() } async fn get_owned<T>( &self, - fn_arg: impl FnOnce(&ContractCacheData) -> T, + fn_arg: impl FnOnce(&MixnetContractCacheData) -> T, ) -> Result<T, UninitialisedCache> { Ok(fn_arg(&**self.inner.get().await?)) } async fn get<'a, T: 'a>( &'a self, - fn_arg: impl FnOnce(&Cache<ContractCacheData>) -> &T, + fn_arg: impl FnOnce(&Cache<MixnetContractCacheData>) -> &T, ) -> Result<RwLockReadGuard<'a, T>, UninitialisedCache> { let guard = self.inner.get().await?; Ok(RwLockReadGuard::map(guard, fn_arg)) @@ -157,12 +161,6 @@ impl NymContractCache { .key_rotation_id(current_absolute_epoch_id)) } - pub(crate) async fn contract_details(&self) -> CachedContractsInfo { - self.get_owned(|c| c.contracts_info.clone()) - .await - .unwrap_or_default() - } - pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { let Ok(cache) = self.inner.get().await else { return Default::default(); @@ -215,6 +213,31 @@ impl NymContractCache { None } + pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { + let mix_denom = self.get_owned(|c| c.rewarding_denom.clone()).await.ok()?; + let reward_pool = self + .interval_reward_params() + .await + .ok()? + .interval + .reward_pool; + + let mixmining_reserve_amount = truncate_decimal(reward_pool).u128(); + let mixmining_reserve = Coin::new(mixmining_reserve_amount, &mix_denom).into(); + + // given all tokens have already vested, the circulating supply is total supply - mixmining reserve + let circulating_supply = + Coin::new(TOTAL_SUPPLY_AMOUNT - mixmining_reserve_amount, &mix_denom).into(); + + Some(CirculatingSupplyResponse { + total_supply: Coin::new(TOTAL_SUPPLY_AMOUNT, &mix_denom).into(), + mixmining_reserve, + // everything has already vested + vesting_tokens: Coin::new(0, &mix_denom).into(), + circulating_supply, + }) + } + pub(crate) async fn naive_wait_for_initial_values(&self) { self.inner.naive_wait_for_initial_values().await } diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/mixnet_contract_cache/cache/refresher.rs similarity index 59% rename from nym-api/src/nym_contract_cache/cache/refresher.rs rename to nym-api/src/mixnet_contract_cache/cache/refresher.rs index 87af6d2d04..7e0735ca21 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/mixnet_contract_cache/cache/refresher.rs @@ -1,9 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::{ - CachedContractInfo, CachedContractsInfo, ConfigScoreData, ContractCacheData, -}; +use crate::mixnet_contract_cache::cache::data::{ConfigScoreData, MixnetContractCacheData}; use crate::nyxd::Client; use crate::support::caching::refresher::CacheItemProvider; use anyhow::Result; @@ -12,9 +10,6 @@ use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; use nym_mixnet_contract_common::LegacyMixLayer; -use nym_validator_client::nyxd::contract_traits::{ - MixnetQueryClient, NymContractsProvider, VestingQueryClient, -}; use nym_validator_client::nyxd::error::NyxdError; use rand::prelude::SliceRandom; use rand::rngs::OsRng; @@ -22,90 +17,29 @@ use std::collections::HashMap; use std::collections::HashSet; use tracing::info; -pub struct ContractDataProvider { +pub struct MixnetContractDataProvider { nyxd_client: Client, } #[async_trait] -impl CacheItemProvider for ContractDataProvider { - type Item = ContractCacheData; +impl CacheItemProvider for MixnetContractDataProvider { + type Item = MixnetContractCacheData; type Error = NyxdError; - async fn try_refresh(&self) -> std::result::Result<Self::Item, Self::Error> { - self.refresh().await + async fn try_refresh(&mut self) -> std::result::Result<Option<Self::Item>, Self::Error> { + self.refresh().await.map(Some) } } -impl ContractDataProvider { +impl MixnetContractDataProvider { pub(crate) fn new(nyxd_client: Client) -> Self { - ContractDataProvider { nyxd_client } + MixnetContractDataProvider { nyxd_client } } - async fn get_nym_contracts_info(&self) -> Result<CachedContractsInfo, NyxdError> { - use crate::query_guard; - - let mut updated = HashMap::new(); - - let client_guard = self.nyxd_client.read().await; - - let mixnet = query_guard!(client_guard, mixnet_contract_address()); - let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); - let group = query_guard!(client_guard, group_contract_address()); - let multisig = query_guard!(client_guard, multisig_contract_address()); - let ecash = query_guard!(client_guard, ecash_contract_address()); - - for (address, name) in [ - (mixnet, "nym-mixnet-contract"), - (vesting, "nym-vesting-contract"), - (coconut_dkg, "nym-coconut-dkg-contract"), - (group, "nym-cw4-group-contract"), - (multisig, "nym-cw3-multisig-contract"), - (ecash, "nym-ecash-contract"), - ] { - let (cw2, build_info) = if let Some(address) = address { - let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); - let mut build_info = query_guard!( - client_guard, - try_get_contract_build_information(address).await - ); - - // for backwards compatibility until we migrate the contracts - if build_info.is_none() { - match name { - "nym-mixnet-contract" => { - build_info = Some(query_guard!( - client_guard, - get_mixnet_contract_version().await - )?) - } - "nym-vesting-contract" => { - build_info = Some(query_guard!( - client_guard, - get_vesting_contract_version().await - )?) - } - _ => (), - } - } - - (cw2, build_info) - } else { - (None, None) - }; - - updated.insert( - name.to_string(), - CachedContractInfo::new(address, cw2, build_info), - ); - } - - Ok(updated) - } - - async fn refresh(&self) -> Result<ContractCacheData, NyxdError> { + async fn refresh(&self) -> Result<MixnetContractCacheData, NyxdError> { let current_reward_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; + let contract_state = self.nyxd_client.get_mixnet_contract_state().await?; let nym_nodes = self.nyxd_client.get_nymnodes().await?; let mixnode_details = self.nyxd_client.get_mixnodes().await?; @@ -184,7 +118,6 @@ impl ContractDataProvider { let key_rotation_state = self.nyxd_client.get_key_rotation_state().await?; let config_score_params = self.nyxd_client.get_config_score_params().await?; let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; - let contracts_info = self.get_nym_contracts_info().await?; info!( "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", @@ -193,7 +126,8 @@ impl ContractDataProvider { nym_nodes.len(), ); - Ok(ContractCacheData { + Ok(MixnetContractCacheData { + rewarding_denom: contract_state.rewarding_denom, legacy_mixnodes, legacy_gateways, nym_nodes, @@ -205,7 +139,6 @@ impl ContractDataProvider { current_reward_params, current_interval, key_rotation_state, - contracts_info, }) } } diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/mixnet_contract_cache/handlers.rs similarity index 100% rename from nym-api/src/nym_contract_cache/handlers.rs rename to nym-api/src/mixnet_contract_cache/handlers.rs diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/mixnet_contract_cache/mod.rs similarity index 50% rename from nym-api/src/nym_contract_cache/mod.rs rename to nym-api/src/mixnet_contract_cache/mod.rs index cdb6bcd8e0..9911c55683 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::ContractCacheData; -use crate::nym_contract_cache::cache::refresher::ContractDataProvider; -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::data::MixnetContractCacheData; +use crate::mixnet_contract_cache::cache::refresher::MixnetContractDataProvider; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::CacheRefresher; use crate::support::{config, nyxd}; use nym_validator_client::nyxd::error::NyxdError; @@ -12,14 +12,14 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) fn build_refresher( - config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + config: &config::MixnetContractCache, + nym_contract_cache_state: &MixnetContractCache, nyxd_client: nyxd::Client, -) -> CacheRefresher<ContractCacheData, NyxdError> { +) -> CacheRefresher<MixnetContractCacheData, NyxdError> { CacheRefresher::new_with_initial_value( - Box::new(ContractDataProvider::new(nyxd_client)), + Box::new(MixnetContractDataProvider::new(nyxd_client)), config.debug.caching_interval, nym_contract_cache_state.inner(), ) - .named("contract-cache-refresher") + .named("mixnet-contract-cache-refresher") } diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index f5a3e12736..4d6c8e01f7 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -5,7 +5,7 @@ use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::{extract, Router}; +use axum::Router; use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; use nym_http_api_common::{FormattedResponse, OutputParams}; @@ -40,7 +40,7 @@ pub(crate) fn nym_network_routes() -> Router<AppState> { )] async fn network_details( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(state): State<AppState>, ) -> FormattedResponse<NetworkDetails> { let output = output.output.unwrap_or_default(); @@ -116,13 +116,18 @@ pub struct ContractInformationContractVersion { )] async fn nym_contracts( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, -) -> FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>> { + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -133,7 +138,7 @@ async fn nym_contracts( ) }) .collect::<HashMap<_, _>>(), - ) + )) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -158,13 +163,18 @@ pub struct ContractInformationBuildInformation { )] async fn nym_contracts_detailed( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, -) -> FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>> { + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -175,5 +185,5 @@ async fn nym_contracts_detailed( ) }) .collect::<HashMap<_, _>>(), - ) + )) } diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index ea1421105f..d0134cafa6 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::{ ReceivedProcessor, ReceivedProcessorReceiver, ReceivedProcessorSender, @@ -13,7 +14,6 @@ use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; use crate::node_describe_cache::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::Config; @@ -38,7 +38,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, @@ -58,7 +58,7 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, } @@ -68,7 +68,7 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, ) -> Self { @@ -178,7 +178,7 @@ impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> { } fn new_packet_preparer( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, @@ -236,7 +236,7 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>( config: &Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index f0d6c4ebfa..49d48ab783 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -1,12 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; @@ -78,7 +78,7 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, @@ -96,7 +96,7 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, diff --git a/nym-api/src/node_describe_cache/provider.rs b/nym-api/src/node_describe_cache/provider.rs index da74b2a5f9..259b14ba1e 100644 --- a/nym-api/src/node_describe_cache/provider.rs +++ b/nym-api/src/node_describe_cache/provider.rs @@ -1,10 +1,10 @@ // Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::refresh::RefreshData; use crate::node_describe_cache::NodeDescribeCacheError; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; @@ -15,7 +15,7 @@ use std::collections::HashMap; use tracing::{error, info}; pub struct NodeDescriptionProvider { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, batch_size: usize, @@ -23,7 +23,7 @@ pub struct NodeDescriptionProvider { impl NodeDescriptionProvider { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, ) -> NodeDescriptionProvider { NodeDescriptionProvider { @@ -49,7 +49,7 @@ impl CacheItemProvider for NodeDescriptionProvider { self.contract_cache.naive_wait_for_initial_values().await } - async fn try_refresh(&self) -> Result<Self::Item, Self::Error> { + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { // we need to query: // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) @@ -110,45 +110,37 @@ impl CacheItemProvider for NodeDescriptionProvider { info!("refreshed self described data for {} nodes", nodes.len()); info!("with {} unique ip addresses", addresses_cache.len()); - Ok(DescribedNodes { + Ok(Some(DescribedNodes { nodes, addresses_cache, - }) + })) } } // currently dead code : ( #[allow(dead_code)] pub(crate) fn new_refresher( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, ) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { CacheRefresher::new( - Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), - ), - config.debug.node_describe_caching_interval, + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), + config.debug.caching_interval, ) } pub(crate) fn new_provider_with_initial_value( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, initial: SharedCache<DescribedNodes>, ) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { CacheRefresher::new_with_initial_value( Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), ), - config.debug.node_describe_caching_interval, + config.debug.caching_interval, initial, ) } diff --git a/nym-api/src/node_performance/contract_cache/data.rs b/nym-api/src/node_performance/contract_cache/data.rs new file mode 100644 index 0000000000..985ee0a4d7 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/data.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::PerformanceRetrievalFailure; +use nym_api_requests::models::RoutingScore; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use nym_validator_client::nyxd::contract_traits::performance_query_client::NodePerformance; +use std::collections::{BTreeMap, HashMap}; + +pub(crate) struct PerformanceContractEpochCacheData { + pub(crate) epoch_id: EpochId, + pub(crate) median_performance: HashMap<NodeId, Performance>, +} + +impl PerformanceContractEpochCacheData { + pub(crate) fn from_node_performance( + performance: Vec<NodePerformance>, + epoch_id: EpochId, + ) -> Self { + let median_performance = performance + .into_iter() + .map(|node_performance| (node_performance.node_id, node_performance.performance)) + .collect(); + PerformanceContractEpochCacheData { + epoch_id, + median_performance, + } + } +} + +pub(crate) struct PerformanceContractCacheData { + pub(crate) epoch_performance: BTreeMap<EpochId, PerformanceContractEpochCacheData>, +} + +impl PerformanceContractCacheData { + pub(crate) fn update( + &mut self, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, + ) { + self.epoch_performance.insert(update.epoch_id, update); + if self.epoch_performance.len() > values_to_retain { + // remove the oldest entry, i.e. one with the lowest epoch id + self.epoch_performance.pop_first(); + } + } + + pub(crate) fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + // TODO: somehow send a signal to refresh this epoch + let epoch_scores = self.epoch_performance.get(&epoch_id).ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!("no cached performance results for epoch {epoch_id}"), + ) + })?; + + let node_score = epoch_scores + .median_performance + .get(&node_id) + .ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!( + "no cached performance results for node {node_id} for epoch {epoch_id}" + ), + ) + })?; + + Ok(RoutingScore::new(node_score.naive_to_f64())) + } +} + +// needed for cache initialisation +impl From<PerformanceContractEpochCacheData> for PerformanceContractCacheData { + fn from(cache_data: PerformanceContractEpochCacheData) -> Self { + let mut epoch_performance = BTreeMap::new(); + epoch_performance.insert(cache_data.epoch_id, cache_data); + PerformanceContractCacheData { epoch_performance } + } +} diff --git a/nym-api/src/node_performance/contract_cache/mod.rs b/nym-api/src/node_performance/contract_cache/mod.rs new file mode 100644 index 0000000000..412b2f00b1 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::contract_cache::refresher::{ + refresher_update_fn, PerformanceContractDataProvider, +}; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use anyhow::bail; +use nym_task::TaskManager; + +pub(crate) mod data; +pub(crate) mod refresher; + +pub(crate) async fn start_cache_refresher( + config: &config::PerformanceProvider, + nyxd_client: nyxd::Client, + mixnet_contract_cache: MixnetContractCache, + task_manager: &TaskManager, +) -> anyhow::Result<SharedCache<PerformanceContractCacheData>> { + let values_to_retain = config.debug.max_epoch_entries_to_retain; + + let mut item_provider = + PerformanceContractDataProvider::new(nyxd_client, mixnet_contract_cache); + + if !item_provider.cache_has_values().await { + bail!("performance contract is empty - can't use it as source of node performance") + } + + let warmed_up_cache = SharedCache::new_with_value( + item_provider + .provide_initial_warmed_up_cache(values_to_retain) + .await?, + ); + + CacheRefresher::new_with_initial_value( + Box::new(item_provider), + config.debug.contract_polling_interval, + warmed_up_cache.clone(), + ) + .named("performance-contract-cache-refresher") + .with_update_fn(move |main_cache, update| { + refresher_update_fn(main_cache, update, values_to_retain) + }) + .start(task_manager.subscribe_named("performance-contract-cache-refresher")); + + Ok(warmed_up_cache) +} diff --git a/nym-api/src/node_performance/contract_cache/refresher.rs b/nym-api/src/node_performance/contract_cache/refresher.rs new file mode 100644 index 0000000000..887374b018 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/refresher.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::{ + PerformanceContractCacheData, PerformanceContractEpochCacheData, +}; +use crate::support::caching::refresher::CacheItemProvider; +use crate::support::nyxd::Client; +use async_trait::async_trait; +use nym_validator_client::nyxd::contract_traits::performance_query_client::LastSubmission; +use nym_validator_client::nyxd::error::NyxdError; +use std::collections::BTreeMap; + +pub struct PerformanceContractDataProvider { + nyxd_client: Client, + mixnet_contract_cache: MixnetContractCache, + last_submission: Option<LastSubmission>, +} + +pub(crate) fn refresher_update_fn( + main_cache: &mut PerformanceContractCacheData, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, +) { + main_cache.update(update, values_to_retain); +} + +#[async_trait] +impl CacheItemProvider for PerformanceContractDataProvider { + type Item = PerformanceContractEpochCacheData; + type Error = NyxdError; + + async fn wait_until_ready(&self) { + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await + } + + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { + self.refresh().await + } +} + +impl PerformanceContractDataProvider { + pub(crate) fn new(nyxd_client: Client, mixnet_contract_cache: MixnetContractCache) -> Self { + PerformanceContractDataProvider { + nyxd_client, + mixnet_contract_cache, + last_submission: None, + } + } + + pub(crate) async fn cache_has_values(&self) -> bool { + let Ok(last_submitted) = self + .nyxd_client + .get_last_performance_contract_submission() + .await + else { + return false; + }; + last_submitted.data.is_some() + } + + pub(crate) async fn provide_initial_warmed_up_cache( + &mut self, + values_to_keep: usize, + ) -> Result<PerformanceContractCacheData, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await; + + // SAFETY: we just waited for cache to be available + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let last = current_epoch.saturating_sub(values_to_keep as u32); + + let mut epoch_performance = BTreeMap::default(); + for epoch in current_epoch..last { + let performance = self.nyxd_client.get_full_epoch_performance(epoch).await?; + let per_epoch_performance = + PerformanceContractEpochCacheData::from_node_performance(performance, epoch); + epoch_performance.insert(epoch, per_epoch_performance); + } + + self.last_submission = Some(last_submitted); + + Ok(PerformanceContractCacheData { epoch_performance }) + } + + async fn refresh(&mut self) -> Result<Option<PerformanceContractEpochCacheData>, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + // no updates + if let Some(prior_submission) = &self.last_submission { + if prior_submission == &last_submitted { + return Ok(None); + } + } + + // SAFETY: refresher is not started until the mixnet contract cache had been initialised + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let performance = self + .nyxd_client + .get_full_epoch_performance(current_epoch) + .await?; + + self.last_submission = Some(last_submitted); + + Ok(Some( + PerformanceContractEpochCacheData::from_node_performance(performance, current_epoch), + )) + } +} diff --git a/nym-api/src/node_performance/mod.rs b/nym-api/src/node_performance/mod.rs new file mode 100644 index 0000000000..f7f5eb5dd1 --- /dev/null +++ b/nym-api/src/node_performance/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod contract_cache; +pub(crate) mod provider; diff --git a/nym-api/src/node_performance/provider/contract_provider.rs b/nym-api/src/node_performance/provider/contract_provider.rs new file mode 100644 index 0000000000..29923ccd20 --- /dev/null +++ b/nym-api/src/node_performance/provider/contract_provider.rs @@ -0,0 +1,94 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::provider::{NodesRoutingScores, PerformanceRetrievalFailure}; +use crate::support::caching::cache::SharedCache; +use crate::support::config; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use tracing::warn; + +pub(crate) struct ContractPerformanceProvider { + cached: SharedCache<PerformanceContractCacheData>, + max_epochs_fallback: u32, +} + +impl ContractPerformanceProvider { + pub(crate) fn new( + config: &config::PerformanceProvider, + contract_cache: SharedCache<PerformanceContractCacheData>, + ) -> Self { + ContractPerformanceProvider { + cached: contract_cache, + max_epochs_fallback: config.debug.max_performance_fallback_epochs, + } + } + + fn node_routing_score_with_fallback( + &self, + contract_cache: &PerformanceContractCacheData, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let err = match contract_cache.node_routing_score(node_id, epoch_id) { + Ok(res) => return Ok(res), + Err(err) => err, + }; + + warn!("failed to retrieve performance score of node {node_id} for epoch {epoch_id}. falling back to at most {} past epochs", self.max_epochs_fallback); + + let threshold = epoch_id.saturating_sub(self.max_epochs_fallback); + let start = epoch_id.saturating_sub(1); + for epoch_id in start..threshold { + if let Ok(res) = contract_cache.node_routing_score(node_id, epoch_id) { + return Ok(res); + } + } + + Err(err) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id) + } + + pub(crate) async fn node_routing_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let Some(first) = node_ids.first() else { + return Ok(NodesRoutingScores::empty()); + }; + + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + *first, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + let mut scores = HashMap::new(); + for node_id in node_ids { + let score = self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id); + scores.insert(node_id, score); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_performance/provider/legacy_storage_provider.rs b/nym-api/src/node_performance/provider/legacy_storage_provider.rs new file mode 100644 index 0000000000..99a886a19b --- /dev/null +++ b/nym-api/src/node_performance/provider/legacy_storage_provider.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::provider::PerformanceRetrievalFailure; +use crate::support::caching::cache::UninitialisedCache; +use crate::support::storage::NymApiStorage; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; + +pub(crate) struct LegacyStoragePerformanceProvider { + storage: NymApiStorage, + mixnet_contract_cache: MixnetContractCache, +} + +impl LegacyStoragePerformanceProvider { + pub(crate) fn new(storage: NymApiStorage, mixnet_contract_cache: MixnetContractCache) -> Self { + LegacyStoragePerformanceProvider { + storage, + mixnet_contract_cache, + } + } + + async fn map_epoch_id_to_end_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, UninitialisedCache> { + let interval_details = self.mixnet_contract_cache.current_interval().await?; + let duration = interval_details.epoch_length(); + let current_end = interval_details.current_epoch_end(); + let current_id = interval_details.current_epoch_absolute_id(); + + if current_id == epoch_id { + return Ok(current_end.unix_timestamp()); + } + + if current_id < epoch_id { + let diff = epoch_id - current_id; + let end = current_end + diff * duration; + return Ok(end.unix_timestamp()); + } + + // epoch_id > current_id + let diff = current_id - epoch_id; + let end = current_end - diff * duration; + Ok(end.unix_timestamp()) + } + + pub(crate) async fn epoch_id_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, PerformanceRetrievalFailure> { + self.map_epoch_id_to_end_unix_timestamp(epoch_id) + .await + .map_err(|_| { + PerformanceRetrievalFailure::new( + 0, + epoch_id, + "mixnet contract cache has not been initialised yet", + ) + }) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let end_ts = self.epoch_id_unix_timestamp(epoch_id).await?; + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, end_ts) + .await + } + + pub(crate) async fn get_node_routing_score_with_unix_timestamp( + &self, + node_id: NodeId, + epoch_id: EpochId, + end_ts: i64, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let reliability = self + .storage + .get_average_node_reliability_in_the_last_24hrs(node_id, end_ts) + .await + .map_err(|err| PerformanceRetrievalFailure::new(node_id, epoch_id, err.to_string()))?; + + // reliability: 0-100 + // score: 0-1 + let score = reliability / 100.; + Ok(RoutingScore::new(score as f64)) + } +} diff --git a/nym-api/src/node_performance/provider/mod.rs b/nym-api/src/node_performance/provider/mod.rs new file mode 100644 index 0000000000..aadc61a1d5 --- /dev/null +++ b/nym-api/src/node_performance/provider/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use async_trait::async_trait; +use legacy_storage_provider::LegacyStoragePerformanceProvider; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use thiserror::Error; +use tracing::{debug, error}; + +pub(crate) mod contract_provider; +pub(crate) mod legacy_storage_provider; + +#[derive(Debug, Error)] +#[error("failed to retrieve performance score for node {node_id} for epoch {epoch_id}: {error}")] +pub(crate) struct PerformanceRetrievalFailure { + pub(crate) node_id: NodeId, + pub(crate) epoch_id: EpochId, + pub(crate) error: String, +} + +impl PerformanceRetrievalFailure { + pub(crate) fn new(node_id: NodeId, epoch_id: EpochId, error: impl Into<String>) -> Self { + PerformanceRetrievalFailure { + node_id, + epoch_id, + error: error.into(), + } + } +} + +pub(crate) struct NodesRoutingScores { + inner: HashMap<NodeId, Result<RoutingScore, PerformanceRetrievalFailure>>, +} + +impl NodesRoutingScores { + pub(crate) fn empty() -> Self { + NodesRoutingScores { + inner: HashMap::new(), + } + } + pub(crate) fn get_or_log(&self, node_id: NodeId) -> RoutingScore { + match self.inner.get(&node_id) { + Some(Ok(score)) => *score, + Some(Err(err)) => { + debug!("{err}"); + RoutingScore::zero() + } + None => RoutingScore::zero(), + } + } +} + +#[async_trait] +pub(crate) trait NodePerformanceProvider { + /// Obtain a performance/routing score of a particular node for given epoch + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure>; + + /// An optimisation for obtaining node scores of multiple nodes at once + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure>; +} + +#[async_trait] +impl NodePerformanceProvider for ContractPerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + self.node_routing_scores(node_ids, epoch_id).await + } +} + +#[async_trait] +impl NodePerformanceProvider for LegacyStoragePerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let mut scores = HashMap::new(); + + let epoch_timestamp = self.epoch_id_unix_timestamp(epoch_id).await?; + for node_id in node_ids { + scores.insert( + node_id, + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, epoch_timestamp) + .await, + ); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs new file mode 100644 index 0000000000..30f24d0b40 --- /dev/null +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use nym_api_requests::models::{ConfigScore, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::VersionScoreFormulaParams; + +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + +pub(crate) fn calculate_config_score( + config_score_data: &ConfigScoreData, + described_data: Option<&NymNodeDescription>, +) -> ConfigScore { + let Some(described) = described_data else { + return ConfigScore::unavailable(); + }; + + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::<semver::Version>() else { + return ConfigScore::bad_semver(); + }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); + + let runs_nym_node = described.description.build_information.binary_name == "nym-node"; + let accepted_terms_and_conditions = described + .description + .auxiliary_details + .accepted_operator_terms_and_conditions; + + let version_score = if !runs_nym_node || !accepted_terms_and_conditions { + 0. + } else { + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) + }; + + ConfigScore::new( + version_score, + versions_behind, + accepted_terms_and_conditions, + runs_nym_node, + ) +} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 01d1be13bb..73aca9c848 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use self::data::NodeStatusCacheData; +use crate::node_performance::provider::PerformanceRetrievalFailure; use crate::support::caching::cache::UninitialisedCache; use crate::support::caching::Cache; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; @@ -16,9 +17,9 @@ use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; +mod config_score; pub mod data; mod inclusion_probabilities; -mod node_sets; pub mod refresher; #[derive(Debug, Error)] @@ -28,6 +29,9 @@ enum NodeStatusCacheError { #[error("the self-described cache data is not available")] UnavailableDescribedCache, + + #[error(transparent)] + PerformanceRetrievalFailure(#[from] PerformanceRetrievalFailure), } impl From<UninitialisedCache> for NodeStatusCacheError { diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs deleted file mode 100644 index 09ea897c33..0000000000 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::helpers::RewardedSetStatus; -use crate::node_status_api::models::Uptime; -use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; -use crate::nym_contract_cache::cache::data::ConfigScoreData; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; -use crate::support::storage::NymApiStorage; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{ - ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated, - MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, -}; -use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; -use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; -use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; -use tracing::trace; - -pub(super) async fn get_mixnode_reliability_from_storage( - storage: &NymApiStorage, - mix_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_mixnode_reliability_in_the_last_24hrs( - mix_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_gateway_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_gateway_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_node_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_node_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -async fn get_routing_score( - storage: &NymApiStorage, - node_id: NodeId, - typ: DescribedNodeType, - epoch: Interval, -) -> RoutingScore { - let maybe_reliability = match typ { - DescribedNodeType::LegacyMixnode => { - get_mixnode_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::LegacyGateway => { - get_gateway_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::NymNode => { - get_node_reliability_from_storage(storage, node_id, epoch).await - } - }; - // reliability: 0-100 - // score: 0-1 - let reliability = maybe_reliability.unwrap_or_default(); - let score = reliability / 100.; - - trace!("reliability for {node_id}: {maybe_reliability:?}. routing score: {score}"); - RoutingScore::new(score as f64) -} - -fn versions_behind_factor_to_config_score( - versions_behind: u32, - params: VersionScoreFormulaParams, -) -> f64 { - let penalty = params.penalty.naive_to_f64(); - let scaling = params.penalty_scaling.naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) -} - -fn calculate_config_score( - config_score_data: &ConfigScoreData, - described_data: Option<&NymNodeDescription>, -) -> ConfigScore { - let Some(described) = described_data else { - return ConfigScore::unavailable(); - }; - - let node_version = &described.description.build_information.build_version; - let Ok(reported_semver) = node_version.parse::<semver::Version>() else { - return ConfigScore::bad_semver(); - }; - let versions_behind = config_score_data - .config_score_params - .version_weights - .versions_behind_factor( - &reported_semver, - &config_score_data.nym_node_version_history, - ); - - let runs_nym_node = described.description.build_information.binary_name == "nym-node"; - let accepted_terms_and_conditions = described - .description - .auxiliary_details - .accepted_operator_terms_and_conditions; - - let version_score = if !runs_nym_node || !accepted_terms_and_conditions { - 0. - } else { - versions_behind_factor_to_config_score( - versions_behind, - config_score_data - .config_score_params - .version_score_formula_params, - ) - }; - - ConfigScore::new( - version_score, - versions_behind, - accepted_terms_and_conditions, - runs_nym_node, - ) -} - -// TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status( - rewarded_set: &CachedEpochRewardedSet, - node_id: NodeId, -) -> RewardedSetStatus { - if rewarded_set.is_standby(&node_id) { - RewardedSetStatus::Standby - } else if rewarded_set.is_active_mixnode(&node_id) { - RewardedSetStatus::Active - } else { - RewardedSetStatus::Inactive - } -} - -#[deprecated] -pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( - storage: &NymApiStorage, - mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, - interval_reward_params: RewardingParams, - current_interval: Interval, - rewarded_set: &CachedEpochRewardedSet, -) -> HashMap<NodeId, MixNodeBondAnnotated> { - let mut annotated = HashMap::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); - - // If the performance can't be obtained, because the nym-api was not started with - // the monitoring (and hence, storage), then reward estimates will be all zero - let performance = - get_mixnode_reliability_from_storage(storage, mixnode.mix_id(), current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let reward_estimate = compute_reward_estimate( - &mixnode, - performance, - rewarded_set_status, - interval_reward_params, - current_interval, - ); - - let node_performance = storage - .construct_mixnode_report(mixnode.mix_id()) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) - else { - continue; - }; - - let (estimated_operator_apy, estimated_delegators_apy) = - compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - - annotated.insert( - mixnode.mix_id(), - MixNodeBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance, - node_performance, - estimated_operator_apy, - estimated_delegators_apy, - ip_addresses, - }, - ); - } - annotated -} - -#[deprecated] -pub(crate) async fn annotate_legacy_gateways_with_details( - storage: &NymApiStorage, - gateway_bonds: Vec<LegacyGatewayBondWithId>, - current_interval: Interval, -) -> HashMap<NodeId, GatewayBondAnnotated> { - let mut annotated = HashMap::new(); - for gateway_bond in gateway_bonds { - let performance = - get_gateway_reliability_from_storage(storage, gateway_bond.node_id, current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let node_performance = storage - .construct_gateway_report(gateway_bond.node_id) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) - else { - continue; - }; - - annotated.insert( - gateway_bond.node_id, - GatewayBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - gateway_bond, - self_described: None, - performance, - node_performance, - ip_addresses, - }, - ); - } - annotated -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn produce_node_annotations( - storage: &NymApiStorage, - config_score_data: &ConfigScoreData, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], - nym_nodes: &[NymNodeDetails], - rewarded_set: &CachedEpochRewardedSet, - current_interval: Interval, - described_nodes: &DescribedNodes, -) -> HashMap<NodeId, NodeAnnotation> { - let mut annotations = HashMap::new(); - - for legacy_mix in legacy_mixnodes { - let node_id = legacy_mix.mix_id(); - - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyMixnode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_mix.mix_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for legacy_gateway in legacy_gateways { - let node_id = legacy_gateway.node_id; - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyGateway, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_gateway.node_id, - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for nym_node in nym_nodes { - let node_id = nym_node.node_id(); - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::NymNode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - nym_node.node_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - annotations -} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 0666b84594..5232155625 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,15 +2,27 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::cache::node_sets::produce_node_annotations; +use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingScores}; +use crate::node_status_api::cache::config_score::calculate_config_score; +use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; +use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::{ - node_status_api::cache::NodeStatusCacheError, nym_contract_cache::cache::NymContractCache, - storage::NymApiStorage, support::caching::CacheNotification, + mixnet_contract_cache::cache::MixnetContractCache, + node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, }; use ::time::OffsetDateTime; +use cosmwasm_std::Decimal; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + DetailedNodePerformance, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, + NodePerformance, +}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails, RewardingParams}; use nym_task::TaskClient; +use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; @@ -24,31 +36,32 @@ pub struct NodeStatusCacheRefresher { fallback_caching_interval: Duration, // Sources for when refreshing data - contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, - contract_cache_listener: watch::Receiver<CacheNotification>, + mixnet_contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, } impl NodeStatusCacheRefresher { pub(crate) fn new( cache: NodeStatusCache, fallback_caching_interval: Duration, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, ) -> Self { Self { cache, fallback_caching_interval, - contract_cache, + mixnet_contract_cache: contract_cache, described_cache, - contract_cache_listener, + mixnet_contract_cache_listener: contract_cache_listener, describe_cache_listener, - storage, + performance_provider, } } @@ -63,7 +76,7 @@ impl NodeStatusCacheRefresher { trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / describe cache is updated - Ok(_) = self.contract_cache_listener.changed() => { + Ok(_) = self.mixnet_contract_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), _ = shutdown.recv() => { @@ -95,7 +108,8 @@ impl NodeStatusCacheRefresher { } fn caches_available(&self) -> bool { - let contract_cache = *self.contract_cache_listener.borrow() != CacheNotification::Start; + let contract_cache = + *self.mixnet_contract_cache_listener.borrow() != CacheNotification::Start; let describe_cache = *self.describe_cache_listener.borrow() != CacheNotification::Start; let available = contract_cache && describe_cache; @@ -130,19 +144,201 @@ impl NodeStatusCacheRefresher { fallback_interval.reset(); } + #[allow(clippy::too_many_arguments)] + pub(crate) async fn produce_node_annotations( + &self, + config_score_data: &ConfigScoreData, + routing_scores: &NodesRoutingScores, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedEpochRewardedSet, + described_nodes: &DescribedNodes, + ) -> HashMap<NodeId, NodeAnnotation> { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let node_id = legacy_mix.mix_id(); + let routing_score = routing_scores.get_or_log(node_id); + + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for legacy_gateway in legacy_gateways { + let node_id = legacy_gateway.node_id; + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + annotations + } + + #[deprecated] + pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( + &self, + mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, + routing_scores: &NodesRoutingScores, + interval_reward_params: RewardingParams, + ) -> HashMap<NodeId, MixNodeBondAnnotated> { + let mut annotated = HashMap::new(); + for mixnode in mixnodes { + let stake_saturation = mixnode + .rewarding_details + .bond_saturation(&interval_reward_params); + + let uncapped_stake_saturation = mixnode + .rewarding_details + .uncapped_bond_saturation(&interval_reward_params); + + let score = routing_scores.get_or_log(mixnode.mix_id()); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) + else { + continue; + }; + + // legacy node will never get rewarded + let estimated_operator_apy = Decimal::zero(); + let estimated_delegators_apy = Decimal::zero(); + + annotated.insert( + mixnode.mix_id(), + MixNodeBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + mixnode_details: mixnode, + stake_saturation, + uncapped_stake_saturation, + performance: score.legacy_performance(), + node_performance: legacy_report, + estimated_operator_apy, + estimated_delegators_apy, + ip_addresses, + }, + ); + } + annotated + } + + #[deprecated] + pub(crate) async fn annotate_legacy_gateways_with_details( + &self, + gateway_bonds: Vec<LegacyGatewayBondWithId>, + routing_scores: &NodesRoutingScores, + ) -> HashMap<NodeId, GatewayBondAnnotated> { + let mut annotated = HashMap::new(); + for gateway_bond in gateway_bonds { + let score = routing_scores.get_or_log(gateway_bond.node_id); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) + else { + continue; + }; + + annotated.insert( + gateway_bond.node_id, + GatewayBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + gateway_bond, + self_described: None, + performance: score.legacy_performance(), + node_performance: legacy_report, + ip_addresses, + }, + ); + } + annotated + } + /// Refreshes the node status cache by fetching the latest data from the contract cache #[allow(deprecated)] async fn refresh(&self) -> Result<(), NodeStatusCacheError> { info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; - let interval_reward_params = self.contract_cache.interval_reward_params().await?; - let current_interval = self.contract_cache.current_interval().await?; - let rewarded_set = self.contract_cache.rewarded_set_owned().await?; - let gateway_bonds = self.contract_cache.legacy_gateways_all().await; - let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_data = self.contract_cache.maybe_config_score_data().await?; + let mixnode_details = self.mixnet_contract_cache.legacy_mixnodes_all().await; + let interval_reward_params = self.mixnet_contract_cache.interval_reward_params().await?; + let current_interval = self.mixnet_contract_cache.current_interval().await?; + let rewarded_set = self.mixnet_contract_cache.rewarded_set_owned().await?; + let gateway_bonds = self.mixnet_contract_cache.legacy_gateways_all().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; + let config_score_data = self.mixnet_contract_cache.maybe_config_score_data().await?; // Compute inclusion probabilities // (all legacy mixnodes have 0% chance of being selected) @@ -157,37 +353,48 @@ impl NodeStatusCacheRefresher { legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); } - // Create annotated data - let node_annotations = produce_node_annotations( - &self.storage, - &config_score_data, - &mixnode_details, - &gateway_bonds, - &nym_nodes, - &rewarded_set, - current_interval, - &described, - ) - .await; + let all_ids = mixnode_details + .iter() + .map(|m| m.bond_information.mix_id) + .chain( + gateway_bonds + .iter() + .map(|g| g.node_id) + .chain(nym_nodes.iter().map(|n| n.bond_information.node_id)), + ) + .collect::<Vec<_>>(); - let mixnodes_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_mixnodes_nodes_with_details( - &self.storage, - mixnode_details, - interval_reward_params, - current_interval, + // note: any internal errors imply failures for that node in particular + let routing_scores = self + .performance_provider + .get_batch_node_scores(all_ids, current_interval.current_epoch_absolute_id()) + .await?; + + // Create annotated data + let node_annotations = self + .produce_node_annotations( + &config_score_data, + &routing_scores, + &mixnode_details, + &gateway_bonds, + &nym_nodes, &rewarded_set, + &described, ) .await; - let gateways_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_gateways_with_details( - &self.storage, - gateway_bonds, - current_interval, + let mixnodes_annotated = self + .annotate_legacy_mixnodes_nodes_with_details( + mixnode_details, + &routing_scores, + interval_reward_params, ) .await; + let gateways_annotated = self + .annotate_legacy_gateways_with_details(gateway_bonds, &routing_scores) + .await; + // Update the cache self.cache .update( diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 9e4fcba62f..91076e5963 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -1,12 +1,10 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use super::reward_estimate::compute_reward_estimate; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; +use crate::{MixnetContractCache, NodeStatusCache}; use nym_api_requests::models::{ ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, @@ -14,6 +12,7 @@ use nym_api_requests::models::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; +use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::NodeId; pub(crate) enum RewardedSetStatus { @@ -90,7 +89,7 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, identity: &str, ) -> AxumResult<GatewayUptimeHistoryResponse> { let history = storage @@ -144,7 +143,7 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<MixnodeUptimeHistoryResponse> { let history = storage @@ -179,7 +178,7 @@ pub(crate) async fn _mixnode_core_status_count( } pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, + cache: &MixnetContractCache, mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { @@ -189,14 +188,15 @@ pub(crate) async fn _get_mixnode_status( pub(crate) async fn _get_mixnode_reward_estimation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let status = contract_cache.mixnode_status(mix_id).await; - let mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + // legacy mixnode will never get any rewards + let reward_estimation = RewardEstimate::zero(); let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -205,14 +205,6 @@ pub(crate) async fn _get_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -222,16 +214,18 @@ pub(crate) async fn _get_mixnode_reward_estimation( } pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: &ComputeRewardEstParam, + _: &ComputeRewardEstParam, status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let mut mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + let reward_estimation = RewardEstimate::zero(); + let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -239,60 +233,6 @@ pub(crate) async fn _compute_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => RewardedSetStatus::Active, - Some(false) => RewardedSetStatus::Standby, - None => { - let actual_status = contract_cache.mixnode_status(mix_id).await; - actual_status.into() - } - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -303,7 +243,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation( pub(crate) async fn _get_mixnode_stake_saturation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<StakeSaturationResponse> { let mixnode = status_cache @@ -411,7 +351,7 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); @@ -429,7 +369,7 @@ pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 0b2a0db478..1b3d97f839 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -3,11 +3,12 @@ use self::cache::refresher::NodeStatusCacheRefresher; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::NodePerformanceProvider; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, + mixnet_contract_cache::cache::MixnetContractCache, + support::{self}, }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; @@ -18,7 +19,6 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) mod helpers; pub(crate) mod models; -pub(crate) mod reward_estimate; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -33,10 +33,10 @@ pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); #[allow(clippy::too_many_arguments)] pub(crate) fn start_cache_refresh( config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache_state: &MixnetContractCache, described_cache: &SharedCache<DescribedNodes>, node_status_cache_state: &NodeStatusCache, - storage: storage::NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, nym_contract_cache_listener: watch::Receiver<support::caching::CacheNotification>, described_cache_cache_listener: watch::Receiver<support::caching::CacheNotification>, shutdown: &TaskManager, @@ -48,7 +48,7 @@ pub(crate) fn start_cache_refresh( described_cache.clone(), nym_contract_cache_listener, described_cache_cache_listener, - storage, + performance_provider, ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs deleted file mode 100644 index 6b1798227d..0000000000 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::helpers::RewardedSetStatus; -use cosmwasm_std::Decimal; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardingParams, -}; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::Interval; - -fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { - if pledge_amount.is_zero() { - return Decimal::zero(); - } - let hundred = Decimal::from_ratio(100u32, 1u32); - - epochs_in_year * hundred * reward / pledge_amount -} - -pub fn compute_reward_estimate( - mixnode: &LegacyMixNodeDetailsWithLayer, - performance: Performance, - rewarded_set_status: RewardedSetStatus, - rewarding_params: RewardingParams, - interval: Interval, -) -> RewardEstimate { - if mixnode.is_unbonding() { - return Default::default(); - } - - if performance.is_zero() { - return Default::default(); - } - - let is_active = match rewarded_set_status { - RewardedSetStatus::Active => true, - RewardedSetStatus::Standby => false, - RewardedSetStatus::Inactive => return Default::default(), - }; - - let work_factor = if is_active { - rewarding_params.active_node_work() - } else { - rewarding_params.standby_node_work() - }; - - let node_reward_params = NodeRewardingParameters { - performance, - work_factor, - }; - let node_reward = mixnode - .rewarding_details - .node_reward(&rewarding_params, node_reward_params); - - let node_cost = mixnode - .rewarding_details - .cost_params - .epoch_operating_cost(interval.epochs_in_interval()) - * performance; - - let reward_distribution = mixnode.rewarding_details.determine_reward_split( - node_reward, - performance, - interval.epochs_in_interval(), - ); - - RewardEstimate { - total_node_reward: node_reward, - operator: reward_distribution.operator, - delegates: reward_distribution.delegates, - operating_cost: node_cost, - } -} - -pub fn compute_apy_from_reward( - mixnode: &LegacyMixNodeDetailsWithLayer, - reward_estimate: RewardEstimate, - interval: Interval, -) -> (Decimal, Decimal) { - let epochs_in_year = Decimal::from_ratio(interval.epoch_length_secs(), 3600u64 * 24 * 365); - - let operator = mixnode.rewarding_details.operator; - let total_delegations = mixnode.rewarding_details.delegates; - let estimated_operator_apy = compute_apy(epochs_in_year, reward_estimate.operator, operator); - let estimated_delegators_apy = - compute_apy(epochs_in_year, reward_estimate.delegates, total_delegations); - (estimated_operator_apy, estimated_delegators_apy) -} diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 2f782f6098..4923b70a9f 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -32,7 +32,44 @@ impl<T> SharedCache<T> { SharedCache::default() } - pub(crate) async fn try_update(&self, value: impl Into<T>, typ: &str) -> Result<(), T> { + pub(crate) fn new_with_value(value: T) -> Self { + SharedCache(Arc::new(RwLock::new(CachedItem { + inner: Some(Cache::new(value)), + }))) + } + + pub(crate) async fn try_update_value<S>( + &self, + update: S, + update_fn: impl Fn(&mut T, S), + typ: &str, + ) -> Result<(), S> + where + S: Into<T>, + { + let update_value = update; + let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await + { + Ok(guard) => guard, + Err(_) => { + debug!("failed to obtain write permit for {typ} cache"); + return Err(update_value); + } + }; + + if let Some(ref mut existing) = guard.inner { + existing.update(update_value, update_fn); + } else { + guard.inner = Some(Cache::new(update_value.into())) + }; + Ok(()) + } + + pub(crate) async fn try_overwrite_old_value( + &self, + value: impl Into<T>, + typ: &str, + ) -> Result<(), T> { let value = value.into(); let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await { @@ -107,6 +144,19 @@ impl<T> From<Cache<T>> for CachedItem<T> { } } +// specialised variant of `Cache` for holding maps of values that allow updates to individual entries + +/* + pub(crate) fn partial_update<F>(&mut self, partial_value: impl Into<S>, update_fn: F) + where + F: FnOnce(&mut T, S), + { + update_fn(&mut self.value, partial_value.into()); + self.as_at = OffsetDateTime::now_utc() + } + +*/ + // don't use this directly! // opt for SharedCache<T> instead pub struct Cache<T> { @@ -166,6 +216,11 @@ impl<T> Cache<T> { } } + pub(crate) fn update<S>(&mut self, update: S, update_fn: impl Fn(&mut T, S)) { + update_fn(&mut self.value, update); + self.as_at = OffsetDateTime::now_utc(); + } + // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn unchecked_update(&mut self, value: impl Into<T>) { self.value = value.into(); diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 05e6a3a3d4..b26a918f73 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::{watch, Notify}; use tokio::time::interval; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; pub(crate) type CacheUpdateWatcher = watch::Receiver<CacheNotification>; @@ -28,13 +28,24 @@ impl Default for RefreshRequester { } } -pub struct CacheRefresher<T, E> { +/// Explanation on generics: +/// the internal SharedCache<T> can be updated in two ways +/// by default CacheItemProvider will just provide a T and the internal values will be swapped +/// however, an alternative is to make it provide another value of type S with an explicit update closure +/// this way the cache will be updated with a custom method mutating the existing value +/// the reason for this is to allow partial updates of maps, where we might not want to retrieve +/// the entire value, and we might want to just insert a new entry +pub struct CacheRefresher<T, E, S = T> { name: String, refreshing_interval: Duration, refresh_notification_sender: watch::Sender<CacheNotification>, + // it's not really THAT complex... it's just a boxed function + #[allow(clippy::type_complexity)] + update_fn: Option<Box<dyn Fn(&mut T, S) + Send + Sync>>, + // TODO: the Send + Sync bounds are only required for the `start` method. could we maybe make it less restrictive? - provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, shared_cache: SharedCache<T>, refresh_requester: RefreshRequester, } @@ -46,15 +57,16 @@ pub(crate) trait CacheItemProvider { async fn wait_until_ready(&self) {} - async fn try_refresh(&self) -> Result<Self::Item, Self::Error>; + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error>; } -impl<T, E> CacheRefresher<T, E> +impl<T, E, S> CacheRefresher<T, E, S> where E: std::error::Error, + S: Into<T>, { - pub(crate) fn new( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + pub(crate) fn new_boxed( + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, ) -> Self { let (refresh_notification_sender, _) = watch::channel(CacheNotification::Start); @@ -63,14 +75,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache: SharedCache::new(), refresh_requester: Default::default(), } } + pub(crate) fn new<P>(item_provider: P, refreshing_interval: Duration) -> Self + where + P: CacheItemProvider<Error = E, Item = S> + Send + Sync + 'static, + { + Self::new_boxed(Box::new(item_provider), refreshing_interval) + } + pub(crate) fn new_with_initial_value( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, shared_cache: SharedCache<T>, ) -> Self { @@ -80,12 +100,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache, refresh_requester: Default::default(), } } + #[must_use] + pub(crate) fn with_update_fn( + mut self, + update_fn: impl Fn(&mut T, S) + Send + Sync + 'static, + ) -> Self { + self.update_fn = Some(Box::new(update_fn)); + self + } + #[must_use] pub(crate) fn named(mut self, name: impl Into<String>) -> Self { self.name = name.into(); @@ -105,22 +135,39 @@ where self.shared_cache.clone() } - // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly - // or via `FnMut(&mut T)` closure - async fn do_refresh_cache(&self) { - let mut updated_items = match self.provider.try_refresh().await { - Err(err) => { - error!("{}: failed to refresh the cache: {err}", self.name); - return; - } - Ok(items) => items, - }; - + async fn update_cache(&self, mut update: S, update_fn: impl Fn(&mut T, S)) { let mut failures = 0; + loop { match self .shared_cache - .try_update(updated_items, &self.name) + .try_update_value(update, &update_fn, &self.name) + .await + { + Ok(_) => break, + Err(returned) => { + failures += 1; + update = returned + } + }; + if failures % 10 == 0 { + warn!( + "failed to obtain write permit for {} cache {failures} times in a row!", + self.name + ); + } + + tokio::time::sleep(Duration::from_secs_f32(0.5)).await + } + } + + async fn overwrite_cache(&self, mut updated_items: T) { + let mut failures = 0; + + loop { + match self + .shared_cache + .try_overwrite_old_value(updated_items, &self.name) .await { Ok(_) => break, @@ -138,6 +185,26 @@ where tokio::time::sleep(Duration::from_secs_f32(0.5)).await } + } + + async fn do_refresh_cache(&mut self) { + let updated_items = match self.provider.try_refresh().await { + Err(err) => { + error!("{}: failed to refresh the cache: {err}", self.name); + return; + } + Ok(Some(items)) => items, + Ok(None) => { + debug!("no updates for {} cache this iteration", self.name); + return; + } + }; + + if let Some(update_fn) = self.update_fn.as_ref() { + self.update_cache(updated_items, update_fn).await; + } else { + self.overwrite_cache(updated_items.into()).await; + } if !self.refresh_notification_sender.is_closed() && self @@ -149,7 +216,7 @@ where } } - pub async fn refresh(&self, task_client: &mut TaskClient) { + pub async fn refresh(&mut self, task_client: &mut TaskClient) { info!("{}: refreshing cache state", self.name); tokio::select! { @@ -161,7 +228,7 @@ where } } - pub async fn run(&self, mut task_client: TaskClient) { + pub async fn run(&mut self, mut task_client: TaskClient) { self.provider.wait_until_ready().await; let mut refresh_interval = interval(self.refreshing_interval); @@ -183,10 +250,11 @@ where } } - pub fn start(self, task_client: TaskClient) + pub fn start(mut self, task_client: TaskClient) where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { tokio::spawn(async move { self.run(task_client).await }); } @@ -195,6 +263,7 @@ where where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { let receiver = self.update_watcher(); self.start(task_client); diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 741b2d3f90..bed8c6efaf 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::client::Client; use crate::ecash::comm::QueryCommunicationChannel; use crate::ecash::dkg::controller::keys::{ @@ -11,17 +10,21 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::state::EcashState; use crate::epoch_operations::EpochAdvancer; use crate::key_rotation::KeyRotationController; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use crate::node_performance::provider::legacy_storage_provider::LegacyStoragePerformanceProvider; +use crate::node_performance::provider::NodePerformanceProvider; use crate::node_status_api::handlers::unstable; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::http::{RouterBuilder, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; @@ -30,8 +33,8 @@ use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::m use crate::support::storage::NymApiStorage; use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::{ - circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, - node_status_api, nym_contract_cache, + ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache, + node_performance, node_status_api, }; use anyhow::{bail, Context}; use nym_config::defaults::NymNetworkDetails; @@ -146,10 +149,9 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - let nym_contract_cache_state = NymContractCache::new(); + let mixnet_contract_cache_state = MixnetContractCache::new(); let node_status_cache_state = NodeStatusCache::new(); let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); let described_nodes_cache = SharedCache::<DescribedNodes>::new(); let node_info_cache = unstable::NodeInfoCache::default(); @@ -208,16 +210,14 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> config.address_cache.time_to_live, config.address_cache.capacity, ), - forced_refresh: ForcedRefresh::new( - config.topology_cacher.debug.node_describe_allow_illegal_ips, - ), - nym_contract_cache: nym_contract_cache_state.clone(), + forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), + mixnet_contract_cache: mixnet_contract_cache_state.clone(), node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), storage: storage.clone(), described_nodes_cache: described_nodes_cache.clone(), - network_details, + network_details: network_details.clone(), node_info_cache, + contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), api_status: ApiStatusState::new(signer_information), ecash_state: Arc::new(ecash_state), }); @@ -227,8 +227,8 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); // let cache = refresher.get_shared_cache(); let describe_cache_refresher = node_describe_cache::provider::new_provider_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), + &config.describe_cache, + mixnet_contract_cache_state.clone(), described_nodes_cache.clone(), ) .named("node-self-described-data-refresher"); @@ -238,31 +238,54 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> let describe_cache_watcher = describe_cache_refresher .start_with_watcher(task_manager.subscribe_named("node-self-described-data-refresher")); + let performance_provider = if config.performance_provider.use_performance_contract_data { + if network_details + .network + .contracts + .performance_contract_address + .is_none() + { + bail!("can't use performance contract data without setting the address of the contract") + } + + let performance_contract_cache = node_performance::contract_cache::start_cache_refresher( + &config.performance_provider, + nyxd_client.clone(), + mixnet_contract_cache_state.clone(), + &task_manager, + ) + .await?; + let provider = ContractPerformanceProvider::new( + &config.performance_provider, + performance_contract_cache, + ); + Box::new(provider) as Box<dyn NodePerformanceProvider + Send + Sync> + } else { + Box::new(LegacyStoragePerformanceProvider::new( + storage.clone(), + mixnet_contract_cache_state.clone(), + )) + }; + // start all the caches first - let contract_cache_refresher = nym_contract_cache::build_refresher( - &config.node_status_api, - &nym_contract_cache_state.clone(), + let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( + &config.mixnet_contract_cache, + &mixnet_contract_cache_state.clone(), nyxd_client.clone(), ); let contract_cache_watcher = - contract_cache_refresher.start_with_watcher(task_manager.subscribe()); + mixnet_contract_cache_refresher.start_with_watcher(task_manager.subscribe()); node_status_api::start_cache_refresh( &config.node_status_api, - &nym_contract_cache_state, + &mixnet_contract_cache_state, &described_nodes_cache, &node_status_cache_state, - storage.clone(), + performance_provider, contract_cache_watcher.clone(), describe_cache_watcher, &task_manager, ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); // start dkg task if config.ecash_signer.enabled { @@ -279,12 +302,15 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> )?; } + let has_performance_data = + config.network_monitor.enabled || config.performance_provider.use_performance_contract_data; + // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::<SphinxMessageReceiver>( config, - &nym_contract_cache_state, + &mixnet_contract_cache_state, described_nodes_cache.clone(), node_status_cache_state.clone(), &storage, @@ -294,19 +320,19 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> .await; HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + } - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - EpochAdvancer::start( - nyxd_client, - &nym_contract_cache_state, - &node_status_cache_state, - described_nodes_cache.clone(), - &storage, - &task_manager, - ); - } + // start 'rewarding' if its enabled and there exists source for performance data + if config.rewarding.enabled && has_performance_data { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &mixnet_contract_cache_state, + &node_status_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); } // finally start a background task watching the contract changes and requesting @@ -314,7 +340,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> KeyRotationController::new( describe_cache_refresh_requester, contract_cache_watcher, - nym_contract_cache_state, + mixnet_contract_cache_state, ) .start(task_manager.subscribe_named("KeyRotationController")); diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index cf2161eaa2..8be4ee0bc0 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -50,9 +50,11 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); -const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); -const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); +const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305); +const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS: u32 = 12; +const DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS: usize = 25; pub(crate) const DEFAULT_ADDRESS_CACHE_TTL: Duration = Duration::from_secs(60 * 15); pub(crate) const DEFAULT_ADDRESS_CACHE_CAPACITY: u64 = 1000; @@ -63,6 +65,10 @@ pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; // TODO: make it configurable pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60); +// contract info is changed very infrequently (essentially once per release cycle) +// so this default is more than enough +pub(crate) const DEFAULT_CONTRACT_DETAILS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20; @@ -101,14 +107,22 @@ pub struct Config { pub base: Base, + #[serde(default)] + pub performance_provider: PerformanceProvider, + // TODO: perhaps introduce separate 'path finder' field for all the paths and directories like we have with other configs pub network_monitor: NetworkMonitor, + #[serde(default)] + pub mixnet_contract_cache: MixnetContractCache, + pub node_status_api: NodeStatusAPI, - pub topology_cacher: TopologyCacher, + #[serde(alias = "topology_cacher")] + pub describe_cache: DescribeCache, - pub circulating_supply_cacher: CirculatingSupplyCacher, + #[serde(default)] + pub contracts_info_cache: ContractsInfoCache, pub rewarding: Rewarding, @@ -130,10 +144,12 @@ impl Config { Config { save_path: None, base: Base::new_default(id.as_ref()), + performance_provider: Default::default(), network_monitor: NetworkMonitor::new_default(id.as_ref()), + mixnet_contract_cache: Default::default(), node_status_api: NodeStatusAPI::new_default(id.as_ref()), - topology_cacher: Default::default(), - circulating_supply_cacher: Default::default(), + describe_cache: Default::default(), + contracts_info_cache: Default::default(), rewarding: Default::default(), ecash_signer: EcashSigner::new_default(id.as_ref()), address_cache: Default::default(), @@ -184,7 +200,7 @@ impl Config { self.base.bind_address = http_bind_address } if args.allow_illegal_ips { - self.topology_cacher.debug.node_describe_allow_illegal_ips = true + self.describe_cache.debug.allow_illegal_ips = true } if let Some(address_cache_ttl) = args.address_cache_ttl { self.address_cache.time_to_live = address_cache_ttl; @@ -305,6 +321,98 @@ impl Base { } } +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct ContractsInfoCache { + pub time_to_live: Duration, +} + +impl Default for ContractsInfoCache { + fn default() -> Self { + ContractsInfoCache { + time_to_live: DEFAULT_CONTRACT_DETAILS_CACHE_TTL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct MixnetContractCache { + #[serde(default)] + pub debug: MixnetContractCacheDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for MixnetContractCache { + fn default() -> Self { + MixnetContractCache { + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct MixnetContractCacheDebug { + #[serde(with = "humantime_serde")] + pub caching_interval: Duration, +} + +impl Default for MixnetContractCacheDebug { + fn default() -> Self { + MixnetContractCacheDebug { + caching_interval: DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProvider { + /// Specifies whether this nym-api should attempt to retrieve node performance + /// information from the performance contract. + pub use_performance_contract_data: bool, + + pub debug: PerformanceProviderDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProvider { + fn default() -> Self { + PerformanceProvider { + // to be changed later + use_performance_contract_data: false, + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProviderDebug { + /// Specifies interval of polling the performance contract. Note it is only applicable + /// if the contract data is being used. + /// Further note that if there have been no updates to the cache, the performance overhead is negligible + /// (i.e. there will be only a single query performed to check if anything has changed) + #[serde(with = "humantime_serde")] + pub contract_polling_interval: Duration, + + /// Specify the maximum number of epochs we can fallback to if given epoch's performance data + /// is not available in the contract + pub max_performance_fallback_epochs: u32, + + /// Specify the maximum number of epoch entries to be kept in the cache in case we needed non-current data + // (currently we need an equivalent of full day worth of data for legacy endpoints) + pub max_epoch_entries_to_retain: usize, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProviderDebug { + fn default() -> Self { + PerformanceProviderDebug { + contract_polling_interval: DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL, + max_performance_fallback_epochs: DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS, + max_epoch_entries_to_retain: DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct AddressCacheConfig { pub time_to_live: Duration, @@ -447,76 +555,41 @@ pub struct NodeStatusAPIDebug { impl Default for NodeStatusAPIDebug { fn default() -> Self { NodeStatusAPIDebug { - caching_interval: DEFAULT_NODE_STATUS_CACHE_INTERVAL, + caching_interval: DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL, } } } #[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacher { +pub struct DescribeCache { // pub enabled: bool, // pub paths: TopologyCacherPathfinder, #[serde(default)] - pub debug: TopologyCacherDebug, + pub debug: DescribeCacheDebug, } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacherDebug { +pub struct DescribeCacheDebug { #[serde(with = "humantime_serde")] + #[serde(alias = "node_describe_caching_interval")] pub caching_interval: Duration, - #[serde(with = "humantime_serde")] - pub node_describe_caching_interval: Duration, + #[serde(alias = "node_describe_batch_size")] + pub batch_size: usize, - pub node_describe_batch_size: usize, - - pub node_describe_allow_illegal_ips: bool, + #[serde(alias = "node_describe_allow_illegal_ips")] + pub allow_illegal_ips: bool, } -impl Default for TopologyCacherDebug { +impl Default for DescribeCacheDebug { fn default() -> Self { - TopologyCacherDebug { - caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, - node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, - node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, - node_describe_allow_illegal_ips: false, - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacher { - pub enabled: bool, - - // pub paths: CirculatingSupplyCacherPathfinder, - #[serde(default)] - pub debug: CirculatingSupplyCacherDebug, -} - -impl Default for CirculatingSupplyCacher { - fn default() -> Self { - CirculatingSupplyCacher { - enabled: true, - debug: CirculatingSupplyCacherDebug::default(), - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacherDebug { - #[serde(with = "humantime_serde")] - pub caching_interval: Duration, -} - -impl Default for CirculatingSupplyCacherDebug { - fn default() -> Self { - CirculatingSupplyCacherDebug { - caching_interval: DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL, + DescribeCacheDebug { + caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + allow_illegal_ips: false, } } } diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 8925b3beee..b04956eed8 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -3,9 +3,9 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; +use crate::mixnet_contract_cache::handlers::nym_contract_cache_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::status_routes; -use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; use crate::status; diff --git a/nym-api/src/support/http/state/contract_details.rs b/nym-api/src/support/http/state/contract_details.rs new file mode 100644 index 0000000000..5380a92809 --- /dev/null +++ b/nym-api/src/support/http/state/contract_details.rs @@ -0,0 +1,182 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::nyxd::Client; +use nym_contracts_common::ContractBuildInformation; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, NymContractsProvider, VestingQueryClient, +}; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::AccountId; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +type ContractAddress = String; + +pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; + +#[derive(Clone)] +pub struct CachedContractInfo { + pub(crate) address: Option<AccountId>, + pub(crate) base: Option<cw2::ContractVersion>, + pub(crate) detailed: Option<ContractBuildInformation>, +} + +impl CachedContractInfo { + pub fn new( + address: Option<&AccountId>, + base: Option<cw2::ContractVersion>, + detailed: Option<ContractBuildInformation>, + ) -> Self { + Self { + address: address.cloned(), + base, + detailed, + } + } +} + +#[derive(Clone)] +pub(crate) struct ContractDetailsCache { + cache_ttl: Duration, + inner: Arc<RwLock<ContractDetailsCacheInner>>, +} + +impl ContractDetailsCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ContractDetailsCache { + cache_ttl, + inner: Arc::new(RwLock::new(ContractDetailsCacheInner::new())), + } + } +} + +struct ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: CachedContractsInfo, +} + +impl ContractDetailsCacheInner { + pub(crate) fn new() -> Self { + ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime::UNIX_EPOCH, + cache_value: Default::default(), + } + } + + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } + + async fn retrieve_nym_contracts_info( + &self, + nyxd_client: &Client, + ) -> Result<CachedContractsInfo, NyxdError> { + use crate::query_guard; + + let mut updated = HashMap::new(); + + let client_guard = nyxd_client.read().await; + + let mixnet = query_guard!(client_guard, mixnet_contract_address()); + let vesting = query_guard!(client_guard, vesting_contract_address()); + let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); + let group = query_guard!(client_guard, group_contract_address()); + let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); + let performance = query_guard!(client_guard, performance_contract_address()); + + for (address, name) in [ + (mixnet, "nym-mixnet-contract"), + (vesting, "nym-vesting-contract"), + (coconut_dkg, "nym-coconut-dkg-contract"), + (group, "nym-cw4-group-contract"), + (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), + (performance, "nym-performance-contract"), + ] { + let (cw2, build_info) = if let Some(address) = address { + let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); + let mut build_info = query_guard!( + client_guard, + try_get_contract_build_information(address).await + ); + + // for backwards compatibility until we migrate the contracts + if build_info.is_none() { + match name { + "nym-mixnet-contract" => { + build_info = Some(query_guard!( + client_guard, + get_mixnet_contract_version().await + )?) + } + "nym-vesting-contract" => { + build_info = Some(query_guard!( + client_guard, + get_vesting_contract_version().await + )?) + } + _ => (), + } + } + + (cw2, build_info) + } else { + (None, None) + }; + + updated.insert( + name.to_string(), + CachedContractInfo::new(address, cw2, build_info), + ); + } + + Ok(updated) + } +} + +impl ContractDetailsCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result<CachedContractsInfo, AxumErrorResponse> { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option<CachedContractsInfo> { + let guard = self.inner.read().await; + if guard.is_valid(self.cache_ttl) { + return Some(guard.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result<CachedContractsInfo, AxumErrorResponse> { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if guard.is_valid(self.cache_ttl) { + return Ok(guard.cache_value.clone()); + } + + // 3. attempt to query the chain for the contracts data + let updated_values = guard.retrieve_nym_contracts_info(client).await?; + guard.last_refreshed_at = OffsetDateTime::now_utc(); + guard.cache_value = updated_values.clone(); + + Ok(updated_values) + } +} diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs index b3bcf49216..ec0d2ca9bb 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -1,18 +1,18 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::caching::Cache; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::nyxd::Client; use crate::support::storage; @@ -27,26 +27,58 @@ use std::sync::Arc; use tokio::sync::RwLockReadGuard; pub(crate) mod chain_status; +pub(crate) mod contract_details; pub(crate) mod force_refresh; #[derive(Clone)] pub(crate) struct AppState { // ideally this would have been made generic to make tests easier, // however, it'd be a way bigger change (I tried) + /// Instance of a client used for interacting with the nyx chain. pub(crate) nyxd_client: Client, + + /// Holds information about the latest chain block it has queried. + /// Note, it is not updated on every request. It follows the embedded ttl. pub(crate) chain_status_cache: ChainStatusCache, + /// Holds mapping between a nyx address and tokens/delegations it holds pub(crate) address_info_cache: AddressInfoCache, + + /// Holds information on when nym-nodes requested an explicit request of their self-described data. + /// It is used to prevent DoS by nodes constantly requesting the refresh. pub(crate) forced_refresh: ForcedRefresh, - pub(crate) nym_contract_cache: NymContractCache, + + /// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval. + pub(crate) mixnet_contract_cache: MixnetContractCache, + + /// Holds processed information on network nodes, i.e. performance, config scores, etc. + // TODO: also perhaps redundant? pub(crate) node_status_cache: NodeStatusCache, - pub(crate) circulating_supply_cache: CirculatingSupplyCache, + + /// Holds reference to the persistent storage of this nym-api. pub(crate) storage: storage::NymApiStorage, + + /// Holds information on the self-reported information of nodes, e.g. auxiliary keys they use, + /// ports they announce, etc. pub(crate) described_nodes_cache: SharedCache<DescribedNodes>, + + /// Information about the current network this nym-api is connected to, e.g. contract addresses, + /// endpoints, denominations. pub(crate) network_details: NetworkDetails, + + /// A simple in-memory cache of node information mapping their database id to their node-ids + /// and public keys. Useful (I guess?) for returning information about test routes. + // TODO: do we need it? pub(crate) node_info_cache: unstable::NodeInfoCache, + + /// Cache containing data (build info, versions, etc.) on all nym smart contracts on the network + pub(crate) contract_info_cache: ContractDetailsCache, + + /// Information about this nym-api, i.e. its public key, startup time, etc. pub(crate) api_status: ApiStatusState, + // todo: refactor it into inner: Arc<EcashStateInner> + /// Cache holding data required by the ecash credentials - static signatures, merkle trees, etc. pub(crate) ecash_state: Arc<EcashState>, } @@ -62,19 +94,21 @@ impl FromRef<AppState> for Arc<EcashState> { } } +impl FromRef<AppState> for MixnetContractCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.mixnet_contract_cache.clone() + } +} + impl AppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache + pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache { + &self.mixnet_contract_cache } pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { &self.node_status_cache } - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - pub(crate) fn network_details(&self) -> &NetworkDetails { &self.network_details } @@ -153,7 +187,7 @@ impl AppState { .address_info_cache .collect_balances( self.nyxd_client.clone(), - self.nym_contract_cache.clone(), + self.mixnet_contract_cache.clone(), self.network_details() .network .chain_details diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 41ef7d2bc4..6de33c4738 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -17,10 +17,10 @@ use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, PartialContractDealingData, State}; use nym_coconut_dkg_common::{ dealer::{DealerDetails, DealerDetailsResponse}, - types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, + types::{EncodedBTEPublicKeyWithProof, Epoch}, verification_key::{ContractVKShare, VerificationKeyShare}, }; -use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_config::defaults::NymNetworkDetails; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; @@ -35,14 +35,19 @@ use nym_mixnet_contract_common::{ }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; -use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; +use nym_validator_client::nyxd::contract_traits::performance_query_client::{ + LastSubmission, NodePerformance, +}; +use nym_validator_client::nyxd::contract_traits::{ + PagedDkgQueryClient, PagedPerformanceQueryClient, PerformanceQueryClient, +}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; use nym_validator_client::nyxd::{ contract_traits::{ DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, - PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, + PagedMixnetQueryClient, PagedMultisigQueryClient, }, cosmwasm_client::types::ExecuteResult, BlockResponse, CosmWasmClient, Fee, TendermintRpcClient, @@ -54,7 +59,6 @@ use nym_validator_client::nyxd::{ use nym_validator_client::{ nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, }; -use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; use tendermint::abci::response::Info; @@ -163,10 +167,6 @@ impl Client { } } - pub(crate) async fn chain_details(&self) -> ChainDetails { - nyxd_query!(self, current_chain_details().clone()) - } - pub(crate) async fn get_ecash_contract_address(&self) -> Result<AccountId, EcashError> { nyxd_query!( self, @@ -258,6 +258,12 @@ impl Client { nyxd_query!(self, get_current_interval_details().await) } + pub(crate) async fn get_mixnet_contract_state( + &self, + ) -> Result<nym_mixnet_contract_common::ContractState, NyxdError> { + nyxd_query!(self, get_mixnet_contract_state().await) + } + pub(crate) async fn get_current_epoch_status(&self) -> Result<EpochStatus, NyxdError> { nyxd_query!(self, get_current_epoch_status().await) } @@ -272,31 +278,6 @@ impl Client { nyxd_query!(self, get_rewarded_set().await) } - pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result<u32, NyxdError> { - let guard = self.inner.read().await; - - // the expect is fine as we always construct the client with the vesting contract explicitly set - let vesting_contract = query_guard!( - guard, - vesting_contract_address().expect("vesting contract address is not available") - ); - // TODO: I don't like the usage of the hardcoded value here - let res = query_guard!( - guard, - query_contract_raw(vesting_contract, b"key".to_vec()).await? - ); - if res.is_empty() { - return Ok(0); - } - - serde_json::from_slice(&res).map_err(NyxdError::from) - } - - pub(crate) async fn get_all_vesting_coins( - &self, - ) -> Result<Vec<AccountVestingCoins>, NyxdError> { - nyxd_query!(self, get_all_accounts_vesting_coins().await) - } pub(crate) async fn get_pending_events_count(&self) -> Result<u32, NyxdError> { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -423,6 +404,19 @@ impl Client { ) -> Result<Option<Coin>, NyxdError> { nyxd_query!(self, get_balance(&address, denom.into()).await) } + + pub(crate) async fn get_last_performance_contract_submission( + &self, + ) -> Result<LastSubmission, NyxdError> { + nyxd_query!(self, get_last_submission().await) + } + + pub(crate) async fn get_full_epoch_performance( + &self, + epoch_id: nym_mixnet_contract_common::EpochId, + ) -> Result<Vec<NodePerformance>, NyxdError> { + nyxd_query!(self, get_all_epoch_performance(epoch_id).await) + } } #[async_trait] @@ -508,7 +502,7 @@ impl crate::ecash::client::Client for Client { async fn get_epoch_threshold( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> crate::ecash::error::Result<Option<Threshold>> { Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) } @@ -522,7 +516,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_dealer_details( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<RegisteredDealerDetails> { let dealer = dealer @@ -537,7 +531,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealer_dealings_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<DealerDealingsStatusResponse> { Ok(nyxd_query!( @@ -548,7 +542,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<DealingStatusResponse> { @@ -564,7 +558,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_metadata( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<Option<DealingMetadata>> { @@ -578,7 +572,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_chunk( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, @@ -593,7 +587,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_share( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> Result<Option<ContractVKShare>, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) @@ -601,7 +595,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_shares( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<ContractVKShare>, EcashError> { Ok(nyxd_query!( self, @@ -611,7 +605,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_ecash_clients( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<EcashApiClient>, EcashError> { Ok(self .get_verification_key_shares(epoch_id) diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index b1f294d6d7..710481d41e 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -81,21 +81,6 @@ impl StorageManager { Ok(node_id) } - pub(super) async fn get_gateway_identity_key( - &self, - node_id: NodeId, - ) -> Result<Option<IdentityKey>, sqlx::Error> { - let identity_key = sqlx::query!( - "SELECT identity FROM gateway_details WHERE node_id = ?", - node_id - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.identity); - - Ok(identity_key) - } - /// Tries to obtain identity value of given mixnode given its mix_id /// /// # Arguments @@ -116,62 +101,6 @@ impl StorageManager { Ok(identity_key) } - /// Gets all reliability statuses for mixnode with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_mixnode_statuses_since( - &self, - mix_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM mixnode_status - JOIN mixnode_details - ON mixnode_status.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?; - "#, - mix_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - - /// Gets all reliability statuses for gateway with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_gateway_statuses_since( - &self, - node_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM gateway_status - JOIN gateway_details - ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; - "#, - node_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - /// Gets the historical daily uptime associated with the particular mixnode /// /// # Arguments diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d059ad304d..8bb14a697d 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -9,7 +9,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; -use crate::storage::models::{NodeStatus, TestingRoute}; +use crate::storage::models::TestingRoute; use crate::support::storage::models::{ GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, TestedGatewayStatus, TestedMixnodeStatus, @@ -133,124 +133,6 @@ impl NymApiStorage { Ok(None) } - /// Gets all statuses for particular mixnode that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode to query. - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_mixnode_statuses( - &self, - mix_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_mixnode_statuses_since(mix_id, since) - .await?; - - Ok(statuses) - } - - /// Gets all statuses for particular gateway that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_gateway_statuses( - &self, - node_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_gateway_statuses_since(node_id, since) - .await?; - - Ok(statuses) - } - - /// Tries to construct a status report for mixnode with the specified mix_id. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn construct_mixnode_report( - &self, - mix_id: NodeId, - ) -> Result<MixnodeStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_mixnode_statuses(mix_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::MixnodeReportNotFound { mix_id }); - } - - // determine the number of runs the mixnode should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(MixnodeStatusReport::construct_from_last_day_reports( - now, - mix_id, - mixnode_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - - pub(crate) async fn construct_gateway_report( - &self, - node_id: NodeId, - ) -> Result<GatewayStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_gateway_statuses(node_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { node_id }); - } - - // determine the number of runs the gateway should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(gateway_identity) = self.manager.get_gateway_identity_key(node_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {node_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(GatewayStatusReport::construct_from_last_day_reports( - now, - node_id, - gateway_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - pub(crate) async fn get_mixnode_uptime_history( &self, mix_id: NodeId, diff --git a/nym-api/src/unstable_routes/v1/account/cache.rs b/nym-api/src/unstable_routes/v1/account/cache.rs index 75fe83a1fb..23623b2bf3 100644 --- a/nym-api/src/unstable_routes/v1/account/cache.rs +++ b/nym-api/src/unstable_routes/v1/account/cache.rs @@ -1,6 +1,8 @@ use crate::unstable_routes::v1::account::data_collector::AddressDataCollector; use crate::unstable_routes::v1::account::models::{NyxAccountDelegationDetails, NyxAccountDetails}; -use crate::{node_status_api::models::AxumResult, nym_contract_cache::cache::NymContractCache}; +use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::AxumResult, +}; use moka::{future::Cache, Entry}; use nym_validator_client::nyxd::AccountId; use std::{sync::Arc, time::Duration}; @@ -48,7 +50,7 @@ impl AddressInfoCache { pub(crate) async fn collect_balances( &self, nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, address: &str, account_id: AccountId, diff --git a/nym-api/src/unstable_routes/v1/account/data_collector.rs b/nym-api/src/unstable_routes/v1/account/data_collector.rs index 7d130e9f94..4d29f6dc29 100644 --- a/nym-api/src/unstable_routes/v1/account/data_collector.rs +++ b/nym-api/src/unstable_routes/v1/account/data_collector.rs @@ -3,8 +3,8 @@ use crate::unstable_routes::v1::account::models::NyxAccountDelegationRewardDetails; use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::{AxumErrorResponse, AxumResult}, - nym_contract_cache::cache::NymContractCache, }; use cosmwasm_std::{Coin, Decimal}; use nym_mixnet_contract_common::NodeRewarding; @@ -14,7 +14,7 @@ use tracing::error; pub(crate) struct AddressDataCollector { nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, account_id: AccountId, total_value: u128, operator_rewards: u128, @@ -26,7 +26,7 @@ pub(crate) struct AddressDataCollector { impl AddressDataCollector { pub(crate) fn new( nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, account_id: AccountId, ) -> Self { From d724f94319a9a26aae9e3f8263455fdddb714993 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:19:56 +0200 Subject: [PATCH 42/47] Bump ns-api version --- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 916f30eb1f..aa455a0ac6 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.1.1" +version = "3.1.2" authors.workspace = true repository.workspace = true homepage.workspace = true From 84e10a654c051b48e8fe1b39965b7448249613d7 Mon Sep 17 00:00:00 2001 From: benedettadavico <benedetta.davico@gmail.com> Date: Tue, 1 Jul 2025 15:26:55 +0200 Subject: [PATCH 43/47] Revert "Bump ns-api version" This reverts commit d724f94319a9a26aae9e3f8263455fdddb714993. --- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index aa455a0ac6..916f30eb1f 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.1.2" +version = "3.1.1" authors.workspace = true repository.workspace = true homepage.workspace = true From a7b57d7e5891f9991bb9508fbcc97883558fa706 Mon Sep 17 00:00:00 2001 From: Jack Wampler <jmwample@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:21:50 -0600 Subject: [PATCH 44/47] Make Mix hops optional for Mixnet Client SURBs (#5861) * allow SURBs to be configured without mix hops * gateways require consistency in surb format so if disabling mixnhops - use updated format --- common/client-core/config-types/src/lib.rs | 3 +++ .../real_messages_control/message_handler.rs | 7 +++++++ .../anonymous-replies/src/reply_surb.rs | 17 ++++++++++++++--- common/nymsphinx/src/preparer/mod.rs | 5 ++++- common/wasm/client-core/src/config/mod.rs | 3 +++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 81c47b4219..af6943738e 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -418,6 +418,9 @@ pub struct Traffic { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index c88a6257ce..aa921d60dc 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -105,6 +105,9 @@ pub(crate) struct Config { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. disable_mix_hops: bool, /// Average delay a data packet is going to get delay at a single mixnode. @@ -159,8 +162,12 @@ impl Config { } /// Configure whether messages senders using this config should use mix hops or not when sending messages. + /// + /// This overrides the `use_legacy_sphinx_format` setting as disabled mix hops + /// requires use of the updated SURB packet format. pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self { self.disable_mix_hops = disable_mix_hops; + self.use_legacy_sphinx_format = false; self } } diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 7926c976ed..6f3dd9137e 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -56,6 +56,13 @@ impl ReplySurb { packet_size.plaintext_size() - ack_overhead - ReplySurbKeyDigestAlgorithm::output_size() - 1 } + /// Construct a ResplySurb object. Selects mix hops for the surb unique to this + /// individual construction. + /// + /// If mix hops are disabled, the route will consistency of the recipient + /// (i.e. the ingress hop) only. When `disable_mix_hops` is enabled + /// `use_legacy_surb_format` is ignored as disabled mix hops requires use of + /// the updated SURB format. // TODO: should this return `ReplySURBError` for consistency sake // or keep `NymTopologyError` because it's the only error it can actually return? pub fn construct<R>( @@ -64,17 +71,21 @@ impl ReplySurb { average_delay: Duration, use_legacy_surb_format: bool, topology: &NymRouteProvider, - _disable_mix_hops: bool, // TODO: support SURBs with no mix hops after changes to surb format / construction + disable_mix_hops: bool, ) -> Result<Self, NymTopologyError> where R: RngCore + CryptoRng, { - let route = topology.random_route_to_egress(rng, recipient.gateway())?; + let route = if disable_mix_hops { + topology.empty_route_to_egress(recipient.gateway())? + } else { + topology.random_route_to_egress(rng, recipient.gateway())? + }; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); let mut surb_material = SURBMaterial::new(route, delays, destination); - if use_legacy_surb_format { + if use_legacy_surb_format && !disable_mix_hops { surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION) } diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 9466ebe716..038f1c4b7a 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -335,6 +335,9 @@ pub struct MessagePreparer<R> { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabled mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } @@ -388,7 +391,7 @@ where self.average_packet_delay, use_legacy_reply_surb_format, topology, - disabled_mix_hops, // TODO: support SURBs with no mix hops after changes to surb format / construction + disabled_mix_hops, )? .with_key_rotation(key_rotation); reply_surbs.push(reply_surb) diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 2378b20ba4..b5b3caca52 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -201,6 +201,9 @@ pub struct TrafficWasm { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabeld mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } From 812a8782b40a3e54f848e4347800e51f6f10f3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Tue, 8 Jul 2025 09:09:18 +0100 Subject: [PATCH 45/47] ignore 'Send' responses when claiming bandwidth (#5884) --- .../gateway-client/src/client/mod.rs | 120 +++++++++++++----- .../src/types/text_response.rs | 4 + 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index eeadec5ea1..612349315f 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -272,7 +272,7 @@ impl<C, St> GatewayClient<C, St> { ) -> Result<(), GatewayClientError> { if let Some(shared_key) = self.shared_key() { let encrypted = message.encrypt(&*shared_key)?; - Box::pin(self.send_websocket_message(encrypted)).await?; + Box::pin(self.send_websocket_message_without_response(encrypted)).await?; Ok(()) } else { Err(GatewayClientError::ConnectionInInvalidState) @@ -330,9 +330,80 @@ impl<C, St> GatewayClient<C, St> { } } + /// Attempt to send a websocket message to the gateway without waiting for any response + async fn send_websocket_message_without_response( + &mut self, + msg: impl Into<Message>, + ) -> Result<(), GatewayClientError> { + match self.connection { + SocketState::Available(ref mut conn) => Ok(conn.send(msg.into()).await?), + SocketState::PartiallyDelegated(ref mut partially_delegated) => { + if let Err(err) = partially_delegated.send_without_response(msg.into()).await { + error!("failed to send message without response - {err}..."); + // we must ensure we do not leave the task still active + if let Err(err) = self.recover_socket_connection().await { + error!("... and the delegated stream has also errored out - {err}") + } + Err(err) + } else { + Ok(()) + } + } + SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), + _ => Err(GatewayClientError::ConnectionInInvalidState), + } + } + + // A very nasty hack due to lack of id tags on messages - send a non-sphinx packet websocket + // message and wait until first non 'Send' response within timeout + pub async fn send_websocket_message_with_non_send_response( + &mut self, + msg: impl Into<Message>, + ) -> Result<ServerResponse, GatewayClientError> { + let should_restart_mixnet_listener = if self.connection.is_partially_delegated() { + self.recover_socket_connection().await?; + true + } else { + false + }; + + let conn = match self.connection { + SocketState::Available(ref mut conn) => conn, + SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished), + _ => return Err(GatewayClientError::ConnectionInInvalidState), + }; + conn.send(msg.into()).await?; + + let timeout = sleep(self.cfg.connection.response_timeout_duration); + tokio::pin!(timeout); + + let response = loop { + tokio::select! { + _ = &mut timeout => { + break Err(GatewayClientError::Timeout); + } + // note: the below will also listen for shutdown signals + msg = self.read_control_response() => { + match msg { + Ok(res) => if !res.is_send() { + break Ok(res); + }, + Err(err) => break Err(err), + } + } + } + }; + + if should_restart_mixnet_listener { + self.start_listening_for_mixnet_messages()?; + } + response + } + + /// Attempt to send a websocket message to the gateway and wait until we receive a response. // If we want to send a message (with response), we need to have a full control over the socket, // as we need to be able to write the request and read the subsequent response - pub async fn send_websocket_message( + pub async fn send_websocket_message_with_response( &mut self, msg: impl Into<Message>, ) -> Result<ServerResponse, GatewayClientError> { @@ -387,29 +458,6 @@ impl<C, St> GatewayClient<C, St> { } } - async fn send_websocket_message_without_response( - &mut self, - msg: Message, - ) -> Result<(), GatewayClientError> { - match self.connection { - SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?), - SocketState::PartiallyDelegated(ref mut partially_delegated) => { - if let Err(err) = partially_delegated.send_without_response(msg).await { - error!("failed to send message without response - {err}..."); - // we must ensure we do not leave the task still active - if let Err(err) = self.recover_socket_connection().await { - error!("... and the delegated stream has also errored out - {err}") - } - Err(err) - } else { - Ok(()) - } - } - SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), - _ => Err(GatewayClientError::ConnectionInInvalidState), - } - } - fn check_gateway_protocol( &self, gateway_protocol: Option<u8>, @@ -535,7 +583,10 @@ impl<C, St> GatewayClient<C, St> { .encrypt(legacy_key)?; info!("sending upgrade request and awaiting the acknowledgement back"); - let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? { + let (ciphertext, nonce) = match self + .send_websocket_message_with_response(upgrade_request) + .await? + { ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) @@ -567,7 +618,7 @@ impl<C, St> GatewayClient<C, St> { &mut self, msg: ClientControlRequest, ) -> Result<(), GatewayClientError> { - match self.send_websocket_message(msg).await? { + match self.send_websocket_message_with_response(msg).await? { ServerResponse::Authenticate { protocol_version, status, @@ -717,13 +768,16 @@ impl<C, St> GatewayClient<C, St> { } } + /// Attempt to retrieve the currently supported gateway protocol version of the remote. pub async fn get_gateway_protocol(&mut self) -> Result<u8, GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } match self - .send_websocket_message(ClientControlRequest::SupportedProtocol {}) + .send_websocket_message_with_non_send_response( + ClientControlRequest::SupportedProtocol {}, + ) .await? { ServerResponse::SupportedProtocol { version } => Ok(version), @@ -740,7 +794,10 @@ impl<C, St> GatewayClient<C, St> { credential, self.shared_key.as_ref().unwrap(), )?; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { @@ -758,7 +815,10 @@ impl<C, St> GatewayClient<C, St> { async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index d60f6adcc6..c3418649cf 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -112,6 +112,10 @@ impl ServerResponse { _ => false, } } + + pub fn is_send(&self) -> bool { + matches!(self, ServerResponse::Send { .. }) + } } impl From<ServerResponse> for Message { From 2f5e8e0bcd0ba4233b632a0e79bd2f1d606d236f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Wed, 9 Jul 2025 08:59:55 +0100 Subject: [PATCH 46/47] feat: forbid running mixnode + entry on the same node (#5878) --- nym-node/src/cli/commands/run/mod.rs | 1 + nym-node/src/config/mod.rs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 8b568150ed..703e47662b 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -78,6 +78,7 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { } config }; + config.validate()?; if !config.modes.any_enabled() { warn!("this node is going to run without mixnode or gateway support! consider providing `mode` value"); diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index eca9289872..dfaa7eaef4 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -426,6 +426,13 @@ impl Config { pub fn validate(&self) -> Result<(), NymNodeError> { self.mixnet.validate()?; + // it's not allowed to run mixnode mode alongside entry mode + if self.modes.mixnode && self.modes.entry { + return Err(NymNodeError::config_validation_failure( + "illegal modes configuration - node cannot run as a mixnode and an entry gateway", + )); + } + Ok(()) } } From b11f6c6c7001719f3317b2d0a3079d7d2aeb5043 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:32:46 +0000 Subject: [PATCH 47/47] [DOCs/operators]: Release notes v2025.12-dolcelatte (#5881) * initialise release update * add dev features and bugfixes * add version --------- Co-authored-by: mfahampshire <maxhampshire@pm.me> --- .../circulating-supply.json | 4 +- .../nyx-outputs/circulating-supply.md | 2 +- .../nyx-outputs/epoch-reward-budget.md | 2 +- .../nyx-outputs/nyx-percent-stake.md | 2 +- .../nyx-outputs/nyx-total-stake.md | 2 +- .../nyx-outputs/stake-saturation.md | 2 +- .../nyx-outputs/staking-target.md | 2 +- .../nyx-outputs/staking_supply.md | 2 +- .../nyx-outputs/token-table.md | 6 +- .../api-scraping-outputs/reward-params.json | 8 +- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../docs/pages/operators/changelog.mdx | 147 ++++++++++++++---- .../pages/operators/nodes/nym-node/setup.mdx | 10 +- 13 files changed, 135 insertions(+), 56 deletions(-) diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json index 060eb96030..8d1b16afe2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -5,7 +5,7 @@ }, "mixmining_reserve": { "denom": "unym", - "amount": "187227500568584" + "amount": "185782781887190" }, "vesting_tokens": { "denom": "unym", @@ -13,6 +13,6 @@ }, "circulating_supply": { "denom": "unym", - "amount": "812772499431416" + "amount": "814217218112810" } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 679f542c4e..0b3ee43ca0 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -812_772_499 +814_217_218 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md index cb66fed878..788d05cd23 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -1 +1 @@ -5_200 +5_160 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 39bbb86bce..fe3aa06e04 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.64% +0.69% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index e36dbca0e2..961a5f20a4 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -44.811 +41.235 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index f6f11060bd..40e1984ae2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -1_037_131 +1_038_975 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index 5df8d8b43b..a5045462e5 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -248_911_577 +249_354_023 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md index 5df8d8b43b..a5045462e5 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -1 +1 @@ -248_911_577 +249_354_023 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index e9ed67ebdc..f723dc5285 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 187_227_500 | +| Mixmining Reserve | Tokens releasing for operators rewards | 185_782_781 | | Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 812_772_499 | -| Stake Saturation | Optimal size of node self-bond + delegation | 1_037_131 | +| Circulating Supply | Amount of unlocked tokens | 814_217_218 | +| Stake Saturation | Optimal size of node self-bond + delegation | 1_038_975 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json index d811dfdbce..107af311de 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -1,10 +1,10 @@ { "interval": { - "reward_pool": "187227500568584.066152911712126276", - "staking_supply": "248911577950871.129740670788161327", + "reward_pool": "185782781887190.195531020579422623", + "staking_supply": "249354023047048.00261862494755182", "staking_supply_scale_factor": "0.30625", - "epoch_reward_budget": "5200763904.682890726469769781", - "stake_saturation_point": "1037131574795.296373919461617338", + "epoch_reward_budget": "5160632830.199727653639460539", + "stake_saturation_point": "1038975096029.366677577603948132", "sybil_resistance": "0.3", "active_set_work_factor": "10", "interval_pool_emission": "0.02" diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 3ddd5e309f..9a18ad5a74 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Thursday, June 12th 2025, 11:03:30 UTC +Friday, July 4th 2025, 14:45:02 UTC diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 4ff68694f9..d72f01bccd 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -47,6 +47,85 @@ This page displays a full list of all the changes during our release cycle from <VarInfo /> +## `v2025.12-dolcelatte` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.12-dolcelatte) +- [`nym-node`](nodes/nym-node.mdx) version `1.14.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-07-09T12:50:32.123212812Z +Build Version: 1.14.0 +Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120 +Commit Date: 2025-07-07T15:44:15.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- **[Nym Governator](https://governator.nym.com) is out!** Make sure to check out this new operators governance tool and vote on: + - [**NIP-1:** Proposing NIP as process to propose and log changes to the Nym network](https://governator.nym.com/proposal/prop-9719838a-e86d-4227-8141-dfd5c651ed92) + - [**NIP-2:** Change stake saturation and active set to improve network decentralisation and efficiency](https://governator.nym.com/proposal/prop-8dfa4a28-55b1-45a5-968f-f2897c2670df) + +### Features + +- [Nympool contract](https://github.com/nymtech/nym/pull/5464): This is the first iteration of the nympool contract. It's currently **not yet** integrated with the mixnet contract. + +- [Noise XKpsk3 integration (2025 version)](https://github.com/nymtech/nym/pull/5692) + +- [Key rotation](https://github.com/nymtech/nym/pull/5777) + +- [HTTP Client Retries, Fallbacks, and Redirects](https://github.com/nymtech/nym/pull/5789) + +- [Close sqlite pool before moving or reopening databases](https://github.com/nymtech/nym/pull/5796): Our sqlite db recovery and backup strategies don't work on Windows because the OS prevents moving the already open file. This PR attempts to address this and in principle it attempts to close all connections to the database and ensure that sqlx relinquishes the access to db file(s). Short summary of what this PR introduces: + + 1. Close sqlx connection pools in order to close/release the db file + 2. Poll until there are no more db file descriptors left open. + +- [Replace chrono with time in NS API](https://github.com/nymtech/nym/pull/5811) + +- [Use the same client bandwidth for top up](https://github.com/nymtech/nym/pull/5813) + +- [ Node status dvpn directory](https://github.com/nymtech/nym/pull/5829) + +- [Removing test-net faucet](https://github.com/nymtech/nym/pull/5840) + +- [Amended the buy section](https://github.com/nymtech/nym/pull/5841): Change the wallet to include the buy options for NYM + +- [Remove bity dir](https://github.com/nymtech/nym/pull/5844) + +- [Remove not used old `mock-api`](https://github.com/nymtech/nym/pull/5845) + +- [Remove qa env](https://github.com/nymtech/nym/pull/5847) + +- [Remove/old env references](https://github.com/nymtech/nym/pull/5848) + +- [Updated browser extension piece removal](https://github.com/nymtech/nym/pull/5849): Keep all the `internal-dev wasm` pieces as future examples + - everything previously was going to be removed + - shows functioning `wasm` interaction with the `js` + +### Bugfix + +- [Fixed typo in API endpoint parameter](https://github.com/nymtech/nym/pull/5449) + +- [Adjust heuristic for wireguard peer activity](https://github.com/nymtech/nym/pull/5818) + +- [Url scheme warning log](https://github.com/nymtech/nym/pull/5819): Fix conditions for logging about url scheme. Warns only if the provided urls are not HTTP or HTTPS, as intended. + +- [Bug Fix for Wallet build](https://github.com/nymtech/nym/pull/5820): Revert url used for `connection-tester` to default `url::Url`. + +- [Fix swapped total and circulating supplies](https://github.com/nymtech/nym/pull/5822) + +- [Fixed client route for obtaining v2 list of gateways](https://github.com/nymtech/nym/pull/5859) + +- [Allow gateways to permit authentication from v4 clients](https://github.com/nymtech/nym/pull/5862) + +- [Backwards compat](https://github.com/nymtech/nym/pull/5865) + ## `v2025.11-cheddar` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar) @@ -82,9 +161,9 @@ cargo Profile: release - [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility. -- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800): +- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800): - Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API` - - Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code). + - Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code). - [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802) @@ -115,7 +194,7 @@ cargo Profile: release ### Operators Updates & Tools - [**Reward calculator**](tokenomics/mixnet-rewards#rewards-logic--calculation): A quick app to calculate rewards for any node included in the Mixnet active set -- **Backup your nodes: Stark Industries Solutions and its subsidy PQ.Hosting were added to EU's sanction list**. Everyone running a node on PQ or Stark in Europe should get additional service free of charge and back up their node there. +- **Backup your nodes: Stark Industries Solutions and its subsidy PQ.Hosting were added to EU's sanction list**. Everyone running a node on PQ or Stark in Europe should get additional service free of charge and back up their node there. - **We recommend operators using Stark Industries Solutions and PQ.Hosting to start looking for alternatives.** Here are some useful pages: - [Backup a node](nodes/maintenance#backup-a-node) - [Backup proxy conf](nodes/maintenance#backup-proxy-configuration) @@ -168,10 +247,10 @@ cargo Profile: release - [Downgrade deranged crate to `0.4.0`](https://github.com/nymtech/nym/pull/5746): Downgrade the crate `deranged` from `0.4.1` to `0.4.0`, as `0.4.1` was yanked and is flagged by `cargo audit` - + - [Upgrade prometheus crate to fix security warning](https://github.com/nymtech/nym/pull/5747): Upgrade the `prometheus` crate to bump the version of the `protobuf` crate, which is flagged by `cargo audit` as having a security issue: - + ```bash Crate: protobuf Version: 2.28.0 @@ -186,7 +265,7 @@ cargo Profile: release ``` - [Update `pretty_env_logger` to latest to not depend on unmaintained crate atty](https://github.com/nymtech/nym/pull/5748): The crate `atty` is flagged to be unmaintained and also having some security issues. -- [Remove `pretty_env_logger` and switch remaining crates to use tracing](https://github.com/nymtech/nym/pull/5749) +- [Remove `pretty_env_logger` and switch remaining crates to use tracing](https://github.com/nymtech/nym/pull/5749) ## `v2025.9-appenzeller` @@ -214,7 +293,7 @@ cargo Profile: release ### Features - [Add contains ticketbook data db query](https://github.com/nymtech/nym/pull/5670) -- [Add `/account/{address}`](https://github.com/nymtech/nym/pull/5673): Meant to replace `https://explorer.nymtech.net/api/v1/tmp/unstable/account/{address}` for Explorer v2 +- [Add `/account/{address}`](https://github.com/nymtech/nym/pull/5673): Meant to replace `https://explorer.nymtech.net/api/v1/tmp/unstable/account/{address}` for Explorer v2 ### Bugfix @@ -254,15 +333,15 @@ cargo Profile: release - [Adding fresh nym-api tests and workflow](https://github.com/nymtech/nym/pull/5659) -- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown. +- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown. -- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687): +- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687): - The core of the lifting was done via the migrate command - A lot of API's changed - Improved styling - Pipelines are fixed for all platforms - -- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops. + +- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops. - [Bump the `nym-vpn deb` metapackage to `1.0`](https://github.com/nymtech/nym/pull/5697) @@ -274,7 +353,7 @@ cargo Profile: release - [Peer handle should die more gracefully](https://github.com/nymtech/nym/pull/5704) -- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version +- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version - [Remove inactive peers](https://github.com/nymtech/nym/pull/5721) @@ -301,7 +380,7 @@ cargo Profile: release ### Operators Updates & Tools -- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source. +- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source. - The Nym Squad League Winter Season has concluded. Changes are coming to NSL, including the new NSL council, monthly payments for contributions, revamped reports, and more. Details available in the [Winter Season report](https://forum.nym.com/t/nym-squad-league-winter-season-report/1265). @@ -317,7 +396,7 @@ cargo Profile: release - [Mix throughput tester](https://github.com/nymtech/nym/pull/5661): A utility to measure mixing throughput of a node. Especially since there are some design choices with non-obvious performance penalties. For example 1 bloomfilter per connection vs 1 global bloomfilter with additional locking for replay protection. -- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args` +- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args` - [Bump the patch-updates group across 1 directory with 7 updates](https://github.com/nymtech/nym/pull/5668): @@ -333,8 +412,8 @@ cargo Profile: release - [Improve explorer caching](https://github.com/nymtech/nym/pull/5669): Fix: - `tanstack` caching - - graphs data bug - - "auto" gas fee for redeem rewards + - graphs data bug + - "auto" gas fee for redeem rewards - [Update node versions in CI](https://github.com/nymtech/nym/pull/5677) @@ -346,7 +425,7 @@ cargo Profile: release - [`clippy` for `1.86`](https://github.com/nymtech/nym/pull/5685) -- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata. +- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata. ### Bugfix @@ -382,11 +461,11 @@ cargo Profile: release - [**Nym Explorer v2.1.0**](https://nym.com/explorer) is out as a beta release -- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. +- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. #### Community Tools -Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. +Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. While we are planning to make a page aggregating these tools together, here are some tools released lately by the technical community: @@ -410,7 +489,7 @@ While we are planning to make a page aggregating these tools together, here are - [Bump `nanoid` from `3.3.7` to `3.3.8` in `/documentation/docs`](https://github.com/nymtech/nym/pull/5335): Bumps [`nanoid`](https://github.com/ai/nanoid) -- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) +- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) - [Bump `@octokit/plugin-paginate-rest` and `@actions/github` in `/.github/actions/nym-hash-releases/src`](https://github.com/nymtech/nym/pull/5488): Bumps [`@octokit/plugin-paginate-rest`](https://github.com/octokit/plugin-paginate-rest.js) @@ -435,16 +514,16 @@ While we are planning to make a page aggregating these tools together, here are - `/issued-ticketbooks-on-count/:issuance_date` - `/issued-ticketbooks-challenge-commitment` - `/issued-ticketbooks-data` - + - removed: - `/issued-ticketbooks-challenge` </AccordionTemplate> <AccordionTemplate name={<TestingSteps/>}> Testing Steps Performed: -New issue ticketbook related endpoints work - tickets are issued and consumed as before. +New issue ticketbook related endpoints work - tickets are issued and consumed as before. Redeeming tickets is happening as normal on the gateway side. - + Notes (if any): Chuckles will not require the rewarder binary to be updated; as there will be additional changes to rewarding, this will be extensively tested in the future. </AccordionTemplate> @@ -479,15 +558,15 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Bump `tempfile` from `3.18.0` to `3.19.0`](https://github.com/nymtech/nym/pull/5631): Bumps [`tempfile`](https://github.com/Stebalien/tempfile) - [Rework IPR codec to extract out timer and implement AsyncWrite](https://github.com/nymtech/nym/pull/5632): The goal is to be able to handle back pressure in the vpn client. For that we need to extract out the timer from the codec since we want to move control over that to the mixnet processor. Once the timer is removed we can reformulate the mixnet sender as a `Sink` wrapped in a `FramedWrite` - + - Extract out timer from IPR codec - Implement AsyncWrite on mixnet client sender - Add functions to wait for lanes to clear on `LaneQueueLenghts` - + - [Remove `explorer-api` from the main workspace](https://github.com/nymtech/nym/pull/5635) - [Bump `dtolnay/rust-toolchain` from `1.90.0` to `1.100.0`](https://github.com/nymtech/nym/pull/5638): Bumps [`dtolnay/rust-toolchain`](https://github.com/dtolnay/rust-toolchain) - + - [Bump `zip` from `2.2.2` to `2.4.1`](https://github.com/nymtech/nym/pull/5639): Bumps [`zip`](https://github.com/zip-rs/zip2) - [Add `max_retransmissions` flag on each message](https://github.com/nymtech/nym/pull/5642): Add an optional `max_retransmissions` field on `InputMessage`, to be able to selectively disable or turn down the number of `retransmissions` done for specific packets @@ -503,7 +582,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Update wallet to include Interval Operator Cost and Profit Margin](https://github.com/nymtech/nym/pull/5652): Created a new modal for Interval Operator Cost and Profit Margin changes - Uses existing update node operating cost functionality for old mixnodes (If it works it works) - Prompts a warning that a user "Can send the request as many times as they want but it will only update the last message when the interval changes" - + - [Wallet-revamp to be in line with new nym-theming](https://github.com/nymtech/nym/pull/5653): Updating colour palette to match the [nym.com](https://nym.com) sites: - Used the same font too - Updated icons @@ -512,7 +591,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Revert using `AsyncWrite` sink in IPR](https://github.com/nymtech/nym/pull/5656): Revert the use of the `AsyncWrite` sink in the IPR until it's fully ready. -- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. +- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. ### Bugfix @@ -543,14 +622,14 @@ cargo Profile: release 2. Security Fixes in Dependencies: Address known security vulnerabilities, making updating highly recommended 3. Authorisation & Timestamp Changes: Modifies how timestamps are validated in Auth v2 - this is going to be what the clients use. For example: the clients will start to use v2 variants of everything (prepping the network for the breaking change to come on April 1st which will force clients to only use v2). - + 4. Wireguard exit policy [scripts](https://github.com/nymtech/nym/tree/develop/scripts/wireguard-exit-policy) ### Operators Updates & Tools - [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration) Operators can use a new guide with our scripts to setup wireguard exit policy using IP tables rules. -- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. +- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. - [Virtualising dedicated server with KVM now includes IPv6 setup](nodes/preliminary-steps/vps-setup/advanced#installing-kvm-on-a-server-with-ubuntu-2204): Thanks to [LunarDAO](https://lunardao.net) squad we caught a missing IPv6 configuration in KVM installation guide. Operators can now run a bare metal or dedicated server and virtualise it into VMs each with a fully functional node, having all specs fully under control. @@ -619,17 +698,17 @@ cargo Profile: release - Add `bond_info` & `self_described` to DB `nym_nodes` - Update mixnode & gateway bond status on data refresh - Add `active` column to DB `nym_nodes` - - Use only active & bonded nodes in scraping/testrun tasks + - Use only active & bonded nodes in scraping/testrun tasks - [Rust SDK SURB example: change hardcoded file to tempdir](https://github.com/nymtech/nym/pull/5576): Change hardcoded filepath for client to tempdir -- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. +- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. - [Delete double memo field in send modal](https://github.com/nymtech/nym/pull/5578) - [Bump ring from `0.17.3` to `0.17.13` in /nym-wallet](https://github.com/nymtech/nym/pull/5582): Bumps [ring](https://github.com/briansmith/ring) -- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): +- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): | Package | From | To | | --- | --- | --- | @@ -696,7 +775,7 @@ cargo Profile: release ## `v2025.4-dorina-patched` -Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. +Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched) - [`nym-node`](nodes/nym-node.mdx) version `1.6.2` diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index f46a126498..760b8e5efa 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ## Current version ```sh -ym-node +nym-node Binary Name: nym-node -Build Timestamp: 2025-06-10T09:41:31.291089877Z -Build Version: 1.13.0 -Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51 -Commit Date: 2025-06-10T11:39:20.000000000+02:00 +Build Timestamp: 2025-07-09T12:50:32.123212812Z +Build Version: 1.14.0 +Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120 +Commit Date: 2025-07-07T15:44:15.000000000+02:00 Commit Branch: HEAD rustc Version: 1.86.0 rustc Channel: stable