Compare commits

..

41 Commits

Author SHA1 Message Date
Simon Wicky a08e66a98a cargo lock 2024-05-21 09:53:50 +02:00
Simon Wicky d3b1780fea update dependency to new sphinx release 2024-05-21 09:42:08 +02:00
Simon Wicky cb11c22b5f remove unused sphinx type re-export 2024-05-21 09:41:46 +02:00
Simon Wicky 470cc0010c andrew's suggestion 2024-05-21 09:41:45 +02:00
Simon Wicky 71ad26455f outfox update 2024-05-21 09:41:21 +02:00
Simon Wicky 210a2a2959 dalek and rand crate update 2024-05-21 09:41:21 +02:00
import this 0eb6eb855b [DOCs/operators]: Create a Nym ISP sheet, write gateway-probe guide & clean FnF docs (#4590)
* initialise nym versin of good-bad isps by tor community

* syntax edit

* finalise csv2md

* initialise isp-list page

* add last update info

* add python3 plugis to ci/cd runnners

* remove argparse installation

* python3 syntax edit

* pip install attempt

* pip install attempt

* change python version in cmdrun command

* fix python modules installation

* correct command python version

* workflows python env update

* add probe guide, clean FnF pages

* fix typos
2024-05-20 12:27:18 +00:00
Jon Häggblad a06ae48e2f Add methods to MixnetClient to sign messages (#4602) 2024-05-20 10:22:28 +02:00
Fouad e5f41731ae Explorer NextJS Rebuild (#4534)
* bootstrap next app + add overview page

* fix AssetList type

* fix up nav stuff

* Refactor Nav component and add network components pages

* Refactor WorldMap component and update TelegramIcon, GitHubIcon, NymVpnIcon, DiscordIcon, and TwitterIcon components

* add service providers page

* mixnodes page

* delegations page + use material react table for all tables

* nodes map page

* Refactor StyledLink component and remove unnecessary console.log statements

* Refactor ESLint configuration, remove unused dependencies, and update component imports

* update deps

* Refactor imports and update dependencies

* fix dark mode

* build single mixnode page

* build single gateway page

* Refactor handleOnDelegate function to use useCallback in mixnodes page.tsx

* Add defaults for constants

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2024-05-16 16:12:06 +01:00
Jędrzej Stuczyński a6fda391ae Feature/rewarder voucher issuance (#4548)
* retrieve ed25519 identities of issuers

* signature verification on issued credentials

* wip

* persisting information about verified deposits, any failures and foul plays

* clippy
2024-05-16 10:15:24 +02:00
Tommy Verrall 1ded24dcfc Merge pull request #4586 from nymtech/bugfix/nym-api-noop-nobanner
[bugfix] noop flag for nym-api for nymvisor compatibility
2024-05-15 14:07:30 +01:00
Tommy Verrall 8c42640853 Merge pull request #4591 from nymtech/jon/described-offsettime-parsing
Use rfc3339 for last_polled in described nym-api endpoint
2024-05-15 14:03:42 +01:00
Tommy Verrall 38aabc7983 Merge pull request #4593 from nymtech/feature/extend-max-freepass-validity
change maximum validity of issuable freepass
2024-05-15 13:40:49 +01:00
Tommy Verrall 4324845d29 Merge pull request #4596 from nymtech/update-contract-addr
update mainnet contract addresses
2024-05-15 10:07:08 +01:00
Jędrzej Stuczyński b9524a0f58 Chore/additional helpers (#4585)
* exposed additional helpers

* changes lost in rebasing
2024-05-14 18:33:56 +02:00
Sachin Kamath e7cd417894 update mainnet contract addresses 2024-05-14 21:30:31 +05:30
benedetta davico ca25db845a Merge pull request #4573 from nymtech/feature/axum-upgrade
upgraded axum and related deps to the most recent version
2024-05-14 17:38:15 +02:00
benedetta davico 64a0ce31a8 Merge branch 'develop' into feature/axum-upgrade 2024-05-14 17:21:44 +02:00
Jędrzej Stuczyński a8fe8d9bfb change maximum validity of issuable freepass 2024-05-14 14:35:53 +01:00
Jędrzej Stuczyński c346f145d1 backwards compatibility to fallback to default in case of failures 2024-05-14 10:15:16 +01:00
Jon Häggblad 45dd6f2632 Fix typo for StorageError::ConstraintUnique (#4592) 2024-05-13 17:48:58 +02:00
Jon Häggblad 22d28759ab Explicitly use rfc3339 for last_polled 2024-05-13 15:55:32 +02:00
Jędrzej Stuczyński 890d0f7440 fixed incorrect dependency path for 'axum-extra' 2024-05-13 14:18:33 +01:00
Jędrzej Stuczyński b342eb870e removed explicit drops 2024-05-13 11:31:34 +01:00
Jędrzej Stuczyński fc71e0cafd fixed tests 2024-05-13 11:31:33 +01:00
Tommy Verrall 1ecb57fda0 Merge pull request #4588 from nymtech/jon/explicitly-handle-sqlite-constraint-violation
Explicitly handle constraint unique violation when importing credential
2024-05-13 11:30:31 +01:00
Tommy Verrall 3c1ec82289 Merge pull request #4588 from nymtech/jon/explicitly-handle-sqlite-constraint-violation
Explicitly handle constraint unique violation when importing credential
2024-05-13 09:57:42 +01:00
Tommy Verrall 089e403d87 Merge pull request #4589 from nymtech/master
merge latest master to develop
2024-05-13 09:56:34 +01:00
Tommy Verrall dd2b477cda Merge pull request #4587 from nymtech/jon/lock-files
Update stale lock files
2024-05-13 09:54:52 +01:00
benedetta davico 0902539332 Merge pull request #4583 from nymtech/release/2024.4-nutella
Release 2024.4-nutella to master
2024-05-13 10:27:08 +02:00
Jon Häggblad 0783c532de Explicitly handle constraint unique violation when importing credential 2024-05-13 10:19:01 +02:00
Jon Häggblad 8817ae7805 Update stale lock files 2024-05-13 08:57:49 +02:00
Jędrzej Stuczyński 6a900c3c42 fixed linter issue in nyxd-scraper 2024-05-10 15:05:37 +01:00
Jędrzej Stuczyński 0ba80c9a86 moved startup 'Starting nym api...' message from stdout to stderr 2024-05-10 11:16:10 +01:00
Jędrzej Stuczyński d712b65ec5 [bugfix] noop flag for nym-api for nymvisor compatibility 2024-05-10 11:02:21 +01:00
Tommy Verrall 383b2c1351 Merge pull request #4552 from nymtech/jon/validator-client-rustls
Add rustls-tls to reqwest in validator-client
2024-05-08 16:51:46 +01:00
benedettadavico f0a4350e83 pruning
version bump and changelog updates
2024-05-08 11:01:50 +02:00
Jon Häggblad 6f4b00b5c2 Add default-features = true for tungstenite and non-wasm in client-core 2024-04-30 14:14:44 +02:00
Jon Häggblad d681ad20cf Keep default features off for tungstenite and wasm 2024-04-30 14:14:44 +02:00
Jon Häggblad 5818d58caf tweak feature args to tungstenite 2024-04-30 14:14:42 +02:00
Jon Häggblad da4eab8fdb Add rustls-tls to reqwest in validator-client 2024-04-30 14:14:00 +02:00
234 changed files with 55424 additions and 8177 deletions
+5 -1
View File
@@ -9,7 +9,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo pip3 install pandas tabulate
- name: Install rsync
run: sudo apt-get install rsync
- uses: rlespinasse/github-slug-action@v3.x
+5 -1
View File
@@ -13,7 +13,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo pip3 install pandas tabulate
- name: Install rsync
run: sudo apt-get install rsync
- uses: rlespinasse/github-slug-action@v3.x
+10
View File
@@ -4,6 +4,16 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2024.4-nutella] (2024-05-08)
- [fix] apply disable_poisson_rate from internal NR/IPR cfgs ([#4579])
- updating sign commands to include nym-node ([#4578])
- changed nym-node redirects from 308 'Permanent Redirect' to 303: 'See Other' ([#4572])
[#4579]: https://github.com/nymtech/nym/pull/4579
[#4578]: https://github.com/nymtech/nym/pull/4578
[#4572]: https://github.com/nymtech/nym/pull/4572
## [2024.3-eclipse] (2024-04-22)
- Initial release of the first iteration of the Nym Node
Generated
+1182 -3299
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -25,7 +25,7 @@ bs58 = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive"] }
dirs = "4.0"
log = { workspace = true } # self explanatory
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization
serde_json = { workspace = true }
thiserror = { workspace = true }
+1 -1
View File
@@ -16,7 +16,7 @@ serde_json = { workspace = true }
tap = "1.0.1"
thiserror = { workspace = true }
tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal"] }
rand = "0.7.3"
rand = { workspace = true }
time = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
+1 -1
View File
@@ -9,7 +9,7 @@ license.workspace = true
[dependencies]
bip39 = { workspace = true }
log = { workspace = true }
rand = "0.7.3"
rand = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
+12 -4
View File
@@ -17,7 +17,7 @@ clap = { workspace = true, optional = true }
futures = { workspace = true }
humantime-serde = { workspace = true }
log = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = "0.10.6"
@@ -25,7 +25,6 @@ si-scale = "0.2.2"
tap = "1.0.1"
thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] }
tungstenite = { workspace = true, default-features = false }
tokio = { workspace = true, features = ["macros"] }
time = { workspace = true }
zeroize = { workspace = true }
@@ -74,8 +73,17 @@ workspace = true
features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.20.1"
features = ["rustls-tls-native-roots"]
workspace = true
features = ["rustls-tls-webpki-roots"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tungstenite]
workspace = true
default-features = true
features = ["rustls-tls-webpki-roots"]
[target."cfg(target_arch = \"wasm32\")".dependencies.tungstenite]
workspace = true
default-features = false
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures]
workspace = true
@@ -39,7 +39,7 @@ use log::{debug, error, info, warn};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::encryption;
use nym_crypto::asymmetric::{encryption, identity};
use nym_gateway_client::{
AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter,
};
@@ -670,6 +670,7 @@ where
let self_address = Self::mix_address(&init_res);
let ack_key = init_res.client_keys.ack_key();
let encryption_keys = init_res.client_keys.encryption_keypair();
let identity_keys = init_res.client_keys.identity_keypair();
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
@@ -792,6 +793,7 @@ where
Ok(BaseClient {
address: self_address,
identity_keys,
client_input: ClientInputStatus::AwaitingProducer {
client_input: ClientInput {
connection_command_sender: client_connection_tx,
@@ -816,6 +818,7 @@ where
pub struct BaseClient {
pub address: Recipient,
pub identity_keys: Arc<identity::KeyPair>,
pub client_input: ClientInputStatus,
pub client_output: ClientOutputStatus,
pub client_state: ClientState,
+2 -5
View File
@@ -14,7 +14,7 @@ futures = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
tokio = { version = "1.24.1", features = ["macros"] }
si-scale = "0.2.2"
time.workspace = true
@@ -48,10 +48,7 @@ features = ["net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
workspace = true
# the choice of this particular tls feature was arbitrary;
# if you reckon a different one would be more appropriate, feel free to change it
# features = ["native-tls"]
features = ["rustls-tls-native-roots"]
features = ["rustls-tls-webpki-roots"]
# wasm-only dependencies
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
@@ -24,7 +24,6 @@ nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contr
nym-service-provider-directory-common = { path = "../../cosmwasm-smart-contracts/service-provider-directory" }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
nym-http-api-client = { path = "../../../common/http-api-client"}
thiserror = { workspace = true }
log = { workspace = true }
@@ -67,6 +66,14 @@ cosmwasm-std = { workspace = true }
workspace = true
features = ["tokio"]
[target."cfg(target_arch = \"wasm32\")".dependencies.reqwest]
workspace = true
features = ["json"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
workspace = true
features = ["json", "rustls-tls"]
[dev-dependencies]
bip39 = { workspace = true }
cosmrs = { workspace = true, features = ["bip32"] }
+3
View File
@@ -18,4 +18,7 @@ pub enum StorageError {
#[error("No unused credential in database. You need to buy at least one")]
NoCredential,
#[error("Database unique constraint violation. Is the credential already imported?")]
ConstraintUnique,
}
@@ -69,9 +69,21 @@ impl Storage for PersistentStorage {
bandwidth_credential.credential_data,
bandwidth_credential.epoch_id,
)
.await?;
Ok(())
.await
.map_err(|err| {
// There is one error we want to handle specifically.
// Check if database_error is `SqliteError` with code 2067 which
// means UNIQUE constraint violation
if let Some(db_error) = err.as_database_error() {
if db_error.code().map_or(false, |code| code == "2067") {
StorageError::ConstraintUnique
} else {
err.into()
}
} else {
err.into()
}
})
}
async fn get_next_unspent_credential(
+5 -5
View File
@@ -8,11 +8,11 @@ use std::str::FromStr;
use thiserror::Error;
pub use nym_coconut::{
aggregate_signature_shares_and_verify, aggregate_verification_keys, blind_sign, hash_to_scalar,
keygen, prepare_blind_sign, prove_bandwidth_credential, verify_credential, Attribute, Base58,
BlindSignRequest, BlindedSerialNumber, BlindedSignature, Bytable, CoconutError, KeyPair,
Parameters, PrivateAttribute, PublicAttribute, SecretKey, Signature, SignatureShare,
VerificationKey, VerifyCredentialRequest,
aggregate_signature_shares, aggregate_signature_shares_and_verify, aggregate_verification_keys,
blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential,
verify_credential, Attribute, Base58, BlindSignRequest, BlindedSerialNumber, BlindedSignature,
Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey,
Signature, SignatureShare, VerificationKey, VerifyCredentialRequest,
};
pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher";
+1 -1
View File
@@ -23,5 +23,5 @@ nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
[dev-dependencies]
rand = "0.7.3"
rand = "0.8.5"
@@ -12,7 +12,8 @@ use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime, Time};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct FreePassIssuedData {
@@ -77,9 +78,9 @@ impl FreePassIssuanceData {
}
pub fn default_expiry_date() -> OffsetDateTime {
// set it to furthest midnight in the future such as it's no more than a week away,
// set it to the furthest midnight in the future such as it's no more than a week away,
// i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024
(OffsetDateTime::now_utc() + MAX_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
(OffsetDateTime::now_utc() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
}
pub fn expiry_date_attribute(&self) -> &Attribute {
@@ -10,9 +10,9 @@ use crate::coconut::bandwidth::{
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_credentials_interface::{
aggregate_signature_shares_and_verify, hash_to_scalar, prepare_blind_sign, Attribute,
BlindedSerialNumber, BlindedSignature, Parameters, PrivateAttribute, PublicAttribute,
Signature, SignatureShare, VerificationKey,
aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar,
prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters,
PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nym_api::EpochId;
@@ -266,6 +266,13 @@ impl IssuanceBandwidthCredential {
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
}
pub fn unchecked_aggregate_signature_shares(
&self,
shares: &[SignatureShare],
) -> Result<Signature, Error> {
aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError)
}
pub fn aggregate_signature_shares(
&self,
verification_key: &VerificationKey,
+4 -4
View File
@@ -17,9 +17,9 @@ generic-array = { workspace = true, optional = true }
hkdf = { version = "0.12.3", optional = true }
hmac = { version = "0.12.1", optional = true }
cipher = { version = "0.4.3", optional = true }
x25519-dalek = { version = "1.1", optional = true }
ed25519-dalek = { version = "1.0", optional = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true }
x25519-dalek = { version = "2.0.0", optional = true }
ed25519-dalek = { version = "2.1", features = ["rand_core"], optional = true }
rand = { version = "0.8.5", optional = true }
serde_bytes = { version = "0.11.6", optional = true }
serde_crate = { version = "1.0", optional = true, default_features = false, features = ["derive"], package = "serde" }
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
@@ -31,7 +31,7 @@ nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-fea
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
[dev-dependencies]
rand_chacha = "0.2"
rand_chacha = "0.3"
[features]
default = ["sphinx"]
@@ -56,7 +56,7 @@ pub struct KeyPair {
impl KeyPair {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let private_key = x25519_dalek::StaticSecret::new(rng);
let private_key = x25519_dalek::StaticSecret::random_from_rng(rng);
let public_key = (&private_key).into();
KeyPair {
@@ -203,7 +203,7 @@ impl<'a> From<&'a PrivateKey> for PublicKey {
impl PrivateKey {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let x25519_secret = x25519_dalek::StaticSecret::new(rng);
let x25519_secret = x25519_dalek::StaticSecret::random_from_rng(rng);
PrivateKey(x25519_secret)
}
@@ -322,9 +322,7 @@ impl<'a> From<&'a PrivateKey> for nym_sphinx_types::PrivateKey {
#[cfg(feature = "sphinx")]
impl From<nym_sphinx_types::PrivateKey> for PrivateKey {
fn from(private_key: nym_sphinx_types::PrivateKey) -> Self {
let private_key_bytes = private_key.to_bytes();
assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE);
Self::from_bytes(&private_key_bytes).unwrap()
Self(private_key)
}
}
@@ -366,7 +364,7 @@ mod sphinx_key_conversion {
#[test]
fn works_for_backward_conversion() {
for _ in 0..NUM_ITERATIONS {
let (sphinx_private, sphinx_public) = nym_sphinx_types::crypto::keygen();
let (sphinx_private, sphinx_public) = nym_sphinx_types::test_utils::fixtures::keygen();
let private_bytes = sphinx_private.to_bytes();
let public_bytes = sphinx_public.as_bytes();
+21 -15
View File
@@ -1,8 +1,8 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use ed25519_dalek::ed25519::signature::Signature as SignatureTrait;
pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
@@ -30,6 +30,9 @@ pub enum Ed25519RecoveryError {
#[error(transparent)]
MalformedBytes(#[from] SignatureError),
#[error(transparent)]
BytesLengthError(#[from] std::array::TryFromSliceError),
#[error("the base58 representation of the public key was malformed - {source}")]
MalformedPublicKeyString {
#[source]
@@ -64,11 +67,11 @@ pub struct KeyPair {
impl KeyPair {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let ed25519_keypair = ed25519_dalek::Keypair::generate(rng);
let ed25519_signing_key = ed25519_dalek::SigningKey::generate(rng);
KeyPair {
private_key: PrivateKey(ed25519_keypair.secret),
public_key: PublicKey(ed25519_keypair.public),
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
public_key: PublicKey(ed25519_signing_key.verifying_key()),
}
}
@@ -109,7 +112,7 @@ impl PemStorableKeyPair for KeyPair {
/// ed25519 EdDSA Public Key
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct PublicKey(ed25519_dalek::PublicKey);
pub struct PublicKey(ed25519_dalek::VerifyingKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -135,7 +138,9 @@ impl PublicKey {
}
pub fn from_bytes(b: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(PublicKey(ed25519_dalek::PublicKey::from_bytes(b)?))
Ok(PublicKey(ed25519_dalek::VerifyingKey::from_bytes(
b.try_into()?,
)?))
}
pub fn to_base58_string(self) -> String {
@@ -189,7 +194,7 @@ impl<'d> Deserialize<'d> for PublicKey {
where
D: Deserializer<'d>,
{
Ok(PublicKey(ed25519_dalek::PublicKey::deserialize(
Ok(PublicKey(ed25519_dalek::VerifyingKey::deserialize(
deserializer,
)?))
}
@@ -223,14 +228,14 @@ impl Display for PrivateKey {
impl<'a> From<&'a PrivateKey> for PublicKey {
fn from(pk: &'a PrivateKey) -> Self {
PublicKey((&pk.0).into())
PublicKey(SigningKey::from_bytes(&pk.0).verifying_key())
}
}
impl PrivateKey {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let ed25519_secret = ed25519_dalek::SecretKey::generate(rng);
let ed25519_secret = ed25519_dalek::SigningKey::generate(rng).to_bytes();
PrivateKey(ed25519_secret)
}
@@ -240,11 +245,11 @@ impl PrivateKey {
}
pub fn to_bytes(&self) -> [u8; SECRET_KEY_LENGTH] {
self.0.to_bytes()
self.0
}
pub fn from_bytes(b: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(PrivateKey(ed25519_dalek::SecretKey::from_bytes(b)?))
Ok(PrivateKey(b.try_into()?))
}
pub fn to_base58_string(&self) -> String {
@@ -259,9 +264,8 @@ impl PrivateKey {
}
pub fn sign<M: AsRef<[u8]>>(&self, message: M) -> Signature {
let expanded_secret_key = ed25519_dalek::ExpandedSecretKey::from(&self.0);
let public_key: PublicKey = self.into();
let sig = expanded_secret_key.sign(message.as_ref(), &public_key.0);
let signing_key: SigningKey = self.0.into();
let sig = signing_key.sign(message.as_ref());
Signature(sig)
}
@@ -330,7 +334,9 @@ impl Signature {
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?))
Ok(Signature(ed25519_dalek::Signature::from_bytes(
bytes.try_into()?,
)))
}
}
+2 -2
View File
@@ -3,11 +3,11 @@
use crate::asymmetric::encryption;
use crate::hkdf;
#[cfg(feature = "rand")]
use cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use cipher::{Key, KeyIvInit, StreamCipher};
use digest::crypto_common::BlockSizeUser;
use digest::Digest;
#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};
/// Generate an ephemeral encryption keypair and perform diffie-hellman to establish
/// shared key with the remote.
@@ -242,7 +242,7 @@ impl SphinxPacketProcessor {
#[cfg(test)]
mod tests {
use super::*;
use nym_sphinx_types::crypto::keygen;
use nym_sphinx_types::test_utils::fixtures::keygen;
fn fixture() -> SphinxPacketProcessor {
let local_keys = keygen();
+6 -3
View File
@@ -18,9 +18,12 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "";
pub const GROUP_CONTRACT_ADDRESS: &str = "";
pub const MULTISIG_CONTRACT_ADDRESS: &str = "";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "";
pub const GROUP_CONTRACT_ADDRESS: &str =
"n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr";
pub const MULTISIG_CONTRACT_ADDRESS: &str =
"n1txayqfz5g9qww3rlflpg025xd26m9payz96u54x4fe3s2ktz39xqk67gzx";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
"n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mpxjsztfpvx";
pub const EPHEMERA_CONTRACT_ADDRESS: &str = "";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
+1 -1
View File
@@ -8,7 +8,7 @@ license.workspace = true
[dependencies]
futures = { workspace = true }
rand = "0.7.3"
rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+2 -2
View File
@@ -9,8 +9,8 @@ repository = { workspace = true }
[dependencies]
log = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand_distr = "0.3"
rand = { workspace = true }
rand_distr = "0.4"
thiserror = { workspace = true }
nym-sphinx-acknowledgements = { path = "acknowledgements" }
+1 -1
View File
@@ -8,7 +8,7 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
serde_crate = { version = "1.0", optional = true, default_features = false, features = ["derive"], package = "serde" }
generic-array = { workspace = true, optional = true, features = ["serde"] }
thiserror = { workspace = true }
+1 -1
View File
@@ -14,5 +14,5 @@ serde = "1.0" # implementing serialization/deserialization for some types, like
thiserror = { workspace = true }
[dev-dependencies]
rand = "0.7"
rand = "0.8.5"
nym-crypto = { path = "../../crypto", features = ["rand"] }
@@ -8,7 +8,7 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
bs58 = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
@@ -24,4 +24,4 @@ nym-topology = { path = "../../topology" }
version = "0.2.83"
[dev-dependencies]
rand_chacha = "0.2"
rand_chacha = "0.3"
@@ -570,7 +570,7 @@ mod tests {
let mut address_bytes = [0; NODE_ADDRESS_LENGTH];
rng.fill_bytes(&mut address_bytes);
let dummy_private = PrivateKey::new_with_rng(rng);
let dummy_private = PrivateKey::random_from_rng(rng);
let pub_key = (&dummy_private).into();
Node {
address: NodeAddressBytes::from_bytes(address_bytes),
+1 -1
View File
@@ -11,7 +11,7 @@ repository = { workspace = true }
[dependencies]
log = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
thiserror = { workspace = true }
nym-sphinx-addressing = { path = "../addressing" }
+1 -1
View File
@@ -8,7 +8,7 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
thiserror = { workspace = true }
nym-crypto = { path = "../../crypto" }
+9 -9
View File
@@ -130,28 +130,28 @@ impl Decoder for NymCodec {
mod packet_encoding {
use super::*;
use nym_sphinx_types::{
crypto, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
test_utils, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node,
NodeAddressBytes, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
};
fn make_valid_outfox_packet(size: PacketSize) -> NymPacket {
let (_, node1_pk) = crypto::keygen();
let (_, node1_pk) = test_utils::fixtures::keygen();
let node1 = Node::new(
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
node1_pk,
);
let (_, node2_pk) = crypto::keygen();
let (_, node2_pk) = test_utils::fixtures::keygen();
let node2 = Node::new(
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
node2_pk,
);
let (_, node3_pk) = crypto::keygen();
let (_, node3_pk) = test_utils::fixtures::keygen();
let node3 = Node::new(
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
node3_pk,
);
let (_, node4_pk) = crypto::keygen();
let (_, node4_pk) = test_utils::fixtures::keygen();
let node4 = Node::new(
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
node4_pk,
@@ -170,17 +170,17 @@ mod packet_encoding {
}
fn make_valid_sphinx_packet(size: PacketSize) -> NymPacket {
let (_, node1_pk) = crypto::keygen();
let (_, node1_pk) = test_utils::fixtures::keygen();
let node1 = Node::new(
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
node1_pk,
);
let (_, node2_pk) = crypto::keygen();
let (_, node2_pk) = test_utils::fixtures::keygen();
let node2 = Node::new(
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
node2_pk,
);
let (_, node3_pk) = crypto::keygen();
let (_, node3_pk) = test_utils::fixtures::keygen();
let node3 = Node::new(
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
node3_pk,
+1 -1
View File
@@ -8,7 +8,7 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
sphinx-packet = { version = "0.1.0", optional = true }
sphinx-packet = { version = "0.2.0", optional = true }
nym-outfox = { path = "../../../nym-outfox", optional = true }
thiserror = { workspace = true }
+2 -2
View File
@@ -15,13 +15,13 @@ pub use sphinx_packet::{
self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH,
PAYLOAD_KEY_SIZE,
},
crypto::{self, EphemeralSecret, PrivateKey, PublicKey, SharedSecret},
crypto::{self, PrivateKey, PublicKey},
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE},
packet::builder::DEFAULT_PAYLOAD_SIZE,
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURBMaterial, SURB},
Error as SphinxError, ProcessedPacket,
test_utils, Error as SphinxError, ProcessedPacket,
};
#[cfg(feature = "sphinx")]
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
+1 -1
View File
@@ -12,7 +12,7 @@ dirs = "4.0"
futures = { workspace = true }
log = { workspace = true }
pin-project = "1.0"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
reqwest = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization
+1 -1
View File
@@ -14,7 +14,7 @@ documentation = { workspace = true }
[dependencies]
bs58 = { workspace = true }
log = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true, optional = true }
semver = "0.11"
+1 -1
View File
@@ -11,7 +11,7 @@ repository = "https://github.com/nymtech/nym"
[dependencies]
async-trait = { workspace = true }
js-sys = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde-wasm-bindgen = { workspace = true }
thiserror = { workspace = true }
+1 -1
View File
@@ -32,7 +32,7 @@ serde_json = { workspace = true, optional = true }
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
[dev-dependencies]
rand = "0.7.3"
rand = "0.8.5"
nym-crypto = { path = "../crypto", features = ["rand"]}
+285 -349
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -32,7 +32,7 @@ cw-multi-test = { workspace = true }
cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig" }
cw4-group = { path = "../multisig/cw4-group" }
rand_chacha = "0.2"
rand_chacha = "0.3"
[[test]]
name = "coconut-test"
@@ -25,7 +25,7 @@ nym-vesting-contract = { path = "../vesting" }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
# external dependencies
rand_chacha = "0.2"
rand_chacha = "0.3"
[[test]]
name = "mixnet-vesting-test"
+1 -1
View File
@@ -44,7 +44,7 @@ time = { version = "0.3", features = ["macros"] }
semver = { workspace = true, default-features = false }
[dev-dependencies]
rand_chacha = "0.2"
rand_chacha = "0.3"
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
[build-dependencies]
+1 -1
View File
@@ -33,7 +33,7 @@ cw-multi-test = { workspace = true }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
nym-sphinx-addressing = { path = "../../common/nymsphinx/addressing" }
rand = "0.8.5"
rand_chacha = "0.2"
rand_chacha = "0.3"
rstest = "0.17.0"
[features]
@@ -31,7 +31,7 @@ vergen = { version = "=7.4.3", default-features = false, features = ["build", "g
anyhow = "1.0.40"
cw-multi-test = { workspace = true }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
rand_chacha = "0.2"
rand_chacha = "0.3"
rstest = "0.17.0"
[features]
+6 -3
View File
@@ -27,13 +27,15 @@
- [Maintenance](nodes/maintenance.md)
- [Manual Node Upgrade](nodes/manual-upgrade.md)
- [Automatic Node Upgrade: Nymvisor Setup and Usage](nodes/nymvisor-upgrade.md)
- [Performance Testing](testing/performance.md)
- [Node Setup](testing/node-setup.md)
- [Metrics Monitoring](testing/templates.md)
- [Performance Monitoring & Testing](testing/performance.md)
<!--- [Node Setup](testing/node-setup.md)-->
- [Gateway Probe](testing/gateway-probe.md)
- [Prometheus & Grafana](testing/prometheus-grafana.md)
- [ExploreNYM scripts](testing/explorenym-scripts.md)
<!-- - [Run in a Docker](testing/docker-monitor.md) -->
# Troubleshooting
- [VPS Setup](troubleshooting/vps-isp.md)
@@ -56,6 +58,7 @@
- [Exit Gateway](legal/exit-gateway.md)
- [Community Counsel](legal/community-counsel.md)
- [ISP List](legal/isp-list.md)
- [Jurisdictions](legal/jurisdictions.md)
- [Switzerland](legal/swiss.md)
- [United States](legal/united-states.md)
@@ -0,0 +1,8 @@
**ISP**,**Locations**,**Public IPv6**,**Crypto Payments**,**Comments**,**Last Updated**
[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR","Very slow customer support","05/2024"
[BitLaunch](https://bitlaunch.io),"Canada, USA, UK","No","Yes","Expensive. Digial Ocean through BitLanch has IPv6","05/2024"
[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box","Yes","Crypto payments must be done per each server monthly or annually.","05/2024"
[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy","Yes out of the box","No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed","05/2024"
[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore","No","Yes","Issued IP doesnt match the location offered by the provider.","05/2024"
[Njalla](https://nja.la),"Sweden","Yes","Yes","Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.","05/2024"
[HostSailor](https://hostsailor.com),"USA","Yes, based on ticket","Yes","The IPv6 setup needs custom research and is not documented","05/2024"
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
2 [Flokinet](https://flokinet.is) Netherlands, Iceland, Romania,France Yes, needs a ticket and custom setup yes, including XMR Very slow customer support 05/2024
3 [BitLaunch](https://bitlaunch.io) Canada, USA, UK No Yes Expensive. Digial Ocean through BitLanch has IPv6 05/2024
4 [Hostinger](https://hostinger.com) France, Lithuania, India, USA, Brazil Yes, out of the box Yes Crypto payments must be done per each server monthly or annually. 05/2024
5 [Linode](https://linode.com) USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy Yes out of the box No, only through [BitLAunch](https://bitlaunch.io) IPv6 sometimes need to be re-added in Networking tab, no reboot needed 05/2024
6 [Cherry Servers](https://www.cherryservers.com) Lithuania, Netherlands, USA, Singapore No Yes Issued IP doesn’t match the location offered by the provider. 05/2024
7 [Njalla](https://nja.la) Sweden Yes Yes Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. 05/2024
8 [HostSailor](https://hostsailor.com) USA Yes, based on ticket Yes The IPv6 setup needs custom research and is not documented 05/2024
@@ -0,0 +1,25 @@
# Where to host your `nym-node`?
```admonish info
The entire content of this page is under [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/).
```
Inspired by a valuable resource, done by Tor community - [*Good Bad ISPs*](https://community.torproject.org/relay/community-resources/good-bad-isps/), LunarDAO squad initiated a table customised for Nym Exit Gateways operators.
This ISP list is fully managed by Nym operator community and it serves as a space to share their experience of running Exit Gateways on various Internet Service Providers (ISPs). The ISPs greatly differ in regards to services they offer as well as to their openess of hosting exit routing software.
Please share any experiences running a node like policies, complains, legal issues and solutions, discrepancy between offers and reality (bandwidth, IP range, locations) or anything regarding pricing or customer support.
If you came across any legal findings, please share them in our [list of jurisdictions](jurisdictions.md).
While we trust that Nym node operators are honest, we would like to ask everyone to do your own research.
```admonish caution title=""
To edit or add information to the ISP list, make changes to the csv file located [here](https://github.com/nymtech/nym/blob/develop/documentation/operators/src/data/isp-sheet.csv) and submit your edits as a pull request according to [this guide](add-content.md).
```
```admonish note title=""
As of now the list is quite short. When it grows, we can divide it according the localities of the listed ISPs.
```
<!--cmdrun python3 ../../../scripts/csv2md.py ../data/isp-sheet.csv -s 0 -->
@@ -10,9 +10,9 @@ A suboptimally configured VPS often results in a non-functional node. To follow
You will need to rent a VPS to run your node on. One key reason for this is that your node **must be able to send TCP data using both IPv4 and IPv6** (as other nodes you talk to may use either protocol).
Tor community created a very helpful table called [*Good Bad ISPs*](https://community.torproject.org/relay/community-resources/good-bad-isps/), use that one as a guideline for your choice of ISP for your VPS.
Tor community created a very helpful table called [*Good Bad ISPs*](https://community.torproject.org/relay/community-resources/good-bad-isps/), you can use that one as a guideline for your choice of ISP for your VPS.
Currently we run [performance testing](../testing/performance.md) events to find out the best optimization. Sphinx packet decryption is CPU-bound, so more fast cores the better throughput.
**Update:** Nym community started an ISP table called [*Where to host your nym node?*](../legal/isp-list.md), check it out and add your findings!
### `nym-node`
@@ -0,0 +1,79 @@
# Nym Gateway Probe
Nym Node operators running Gateway functionality are already familiar with the monitoring tool [Harbourmaster.nymtech.net](https://harbourmaster.nymtech.net). Under the hood of Nym Harbourmaster runs iterations of `nym-gateway-probe` doing various checks and displaying the results on the interface. Operators don't have to rely on the probe ran by Nym and wait for the data to refresh. With `nym-gateway-probe` everyone can check any Gateway's networking status from their own computer at any time. In one command the client queries data from:
- [`nym-api`](https://validator.nymtech.net/api/)
- [`explorer-api`](https://explorer.nymtech.net/api/)
- [`harbour-master`](https://harbourmaster.nymtech.net/)
## Preparation
We recommend to have install all [the prerequisites](../binaries/building-nym.md#prerequisites) needed to build `nym-node` from source including latest [Rust Toolchain](https://www.rust-lang.org/tools/install).
## Installation
`nym-gateway-probe` source code is in [`nym-vpn-client`](https://github.com/nymtech/nym-vpn-client) repository. The client needs to be build from source.
1. Clone the repository:
```sh
git clone https://github.com/nymtech/nym-vpn-client.git
```
2. Build `nym-gateway-probe`:
```sh
cd nym-vpn-client
cargo build --release -p nym-gateway-probe
```
## Running the client
```sh
./target/release/nym-gateway-probe --help
```
~~~admonish collapsible=true
```
Usage: nym-gateway-probe [OPTIONS]
Options:
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file describing the network
-g, --gateway <GATEWAY>
-n, --no-log
-h, --help Print help
-V, --version Print version
```
~~~
To run the client, simply add a flag `--gateway` with a targeted gateway identity key.
```sh
./target/release/nym-gateway-probe --gateway <GATEWAY_IDENTITY_KEY>
```
For any `nym-node --mode exit-gateway` the aim is to have this outcome:
```sh
{
"gateway": "<GATEWAY_IDENTITY_KEY>",
"outcome": {
"as_entry": {
"can_connect": true,
"can_route": true
},
"as_exit": {
"can_connect": true,
"can_route_ip_v4": true,
"can_route_ip_external_v4": true,
"can_route_ip_v6": true,
"can_route_ip_external_v6": true
}
}
}
```
If you don't provide a `--gateway` flag it will pick a random one to test.
@@ -1,10 +1,14 @@
# Node Setup for Performance Testing Event
```admonish info
For the moment we paused Fast and Furious `perf` environment. Nym Mainnet environment will be used for future tests, please wait for further instructions.
```
To join the [Performance testing event]({{performance_testing_webpage}}) node operators need to do proceed with the following tasks:
1. **[Sign their node]({{performance_testing_webpage}}) into the testing environment**
2. **[Configure their node](#node-configuration) for the test**
3. (*Not mandatory*) [Setup metric monitoring system](templates.md) to observe node performance at any time
3. (*Not mandatory*) [Setup metric monitoring system](performance.md#monitoring) to observe node performance at any time
## Node Configuration
@@ -1,8 +1,34 @@
# Performance Testing
# Performance Monitoring & Testing
> To configure your node for a testing event, visit [node setup page](node-setup.md).
Nym Mixnet has been running on mainnet for quite some time. There is still work to be done in order for the network to meet its full potential - mass adoption of privacy through fully distributed Mixnet.
Nym Mixnet has been running on mainnet for quite some time. There is still work to be done in order for the network to meet its full potential - mass adoption of privacy through fully distributed Mixnet.
As developers we need to be constantly improving the software. Operators have as much important role, keep their nodes up to date, monitor their performance and share their feedback with the rest of the community and core developers.
Therefore [monitoring](#monitoring) and [testing](#testing) are essential pieces of our common work. We call out all Nym operators to join the efforts!
## Monitoring
There are multiple ways to monitor performance of nodes and the machines on which they run. For the purpose of maximal privacy and decentralisation of the data - preventing Nym Mixnet from any global adversary takeover - we created these pages as a source of mutual empowerment, a place where operators can share and learn new skills to **setup metrics monitors on their own infrastructure**.
### Guides to Setup Own Metrics
A list of different scripts, templates and guides for easier navigation:
* [`nym-gateway-probe`](gateway-probe.md) - a useful tool used under the hood of [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net)
* [Prometheus and Grafana](prometheus-grafana.md) self-hosted setup
* [Nym-node CPU cron service](https://gist.github.com/tommyv1987/97e939a7adf491333d686a8eaa68d4bd) - an easy bash script by Nym core developer [@tommy1987](https://gist.github.com/tommyv1987), designed to monitor a CPU usage of your node, running locally
* Nym's script [`prom_targets.py`](https://github.com/nymtech/nym/blob/develop/scripts/prom_targets.py) - a useful python program to request data from API and can be run on its own or plugged to more sophisticated flows
### Collecting Testing Metrics
For the purpose of the performance testing Nym core developers plan to run instances of Prometheus and Grafana connected to Node explorer in the house. The network overall key insights we seek from these tests are primarily internal. We're focused on pinpointing bottlenecks, capacity loads, and monitoring cpu usage on the nodes' machines.
## Testing
```admonish info
For the moment we paused Fast and Furious `perf` environment. Nym Mainnet environment will be used for future tests, please wait for further instructions.
```
Nym asks its decentralised community of operators to join a series of performance testing events in order to **increase the overall quality of the Mixnet**. The main takeaways of such event are:
@@ -21,7 +47,7 @@ Visit [Fast and Furious web page]({{performance_testing_webpage}}) and [Nym Harb
* Nym runs a paralel network environment [validator.performance.nymte.ch]({{performance_validator}}) with a chain ID `perf`
* Operators of Nym Nodes join by following easy steps on [performance testing web page]({{performance_testing_webpage}}), including simplified node authentication signature (while keep running their nodes on the mainnet)
* Once signed in, operators will be asked to swap their binary for the modified version with metrics endpoint to be able to connect their own [monitoring system](templates.md)
* Once signed in, operators will be asked to swap their binary for the modified version with metrics endpoint to be able to connect their own [monitoring system](#monitoring)
* Core node data will be fed to a unique mixnet contract for the `perf` side chain
* Nym starts a new API and start packet transition in high load through these nodes in both settings
* Nym tracks packet flow using Prometheus and Grafana
@@ -31,4 +57,5 @@ Visit [Fast and Furious web page]({{performance_testing_webpage}}) and [Nym Harb
## More Information
* What happens after the test or what operators get for participating is shared up to date on the [performance testing web page]({{performance_testing_webpage}})
* Visit our guides to [setup metrics template](templates.md) and learn how to operate them in self-custodial way
@@ -16,6 +16,8 @@ Begin with the steps listed in [*Connectivity Test and Configuration*](../nodes/
2. Checkout your VPS dashboard and make sure your IPv6-public enabled.
3. If you are able to add IPv6 address `/64` range, do it.
**Update:** Nym community started an ISP table called [*Where to host your nym node?*](../legal/isp-list.md), check it out and add your findings!
![](../images/ipv6_64.png)
4. Search or ask your ISP for additional documentation related to IPv6 routing and ask them to provide you with `IPv6 IP address` and `IPv6 IP gateway address`
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/python3
"""CLI to display .csv files as markdown"""
import argparse
import pandas as pd
import sys
import csv
def create_table(args):
"""Imports csv and creates a table"""
file = args.file
csv = pd.read_csv(file)
if args.sort != None:
csv = csv.sort_values(csv.columns[args.sort])
if args.table:
table = csv.to_markdown(tablefmt="grid", index=args.index)
else:
table = csv.to_markdown(index=args.index)
return table
def display_file(args):
"""Display csv file as a table"""
table = create_table(args)
print(table)
def panic(msg):
"""Error message print"""
print(f"error: {msg}", file=sys.stderr)
sys.exit(-1)
def parser_main():
"""Main function initializing ArgumentParser, storing arguments and executing commands."""
# Top level parser
parser = argparse.ArgumentParser(
prog='CSV2MD',
description='''Displays .csv files in markdown''',
epilog='''Code is power!'''
)
# Parser arguments
parser.add_argument("-V","--version", action="version", version='%(prog)s 1.1.0')
parser.add_argument("file", help="path/to/file.csv")
parser.add_argument("-t","--table", default=False, action="store_true", help="output with a tabulate option for terminal reading - does not render in mdbook")
parser.add_argument("-i","--index", default=False, action="store_true", help="output with an index column")
parser.add_argument("-s","--sort", type=int, help="supply with column index to sort your output accordingly (ascending way)")
parser.set_defaults(func=display_file)
args = parser.parse_args()
try:
args.func(args)
except AttributeError as e:
msg = f"{e}.\nPlease run with --help or read the error message in case your .csv file is corrupted."
panic(msg)
if __name__ == '__main__':
parser_main()
+3
View File
@@ -14,6 +14,9 @@ DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr
VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw
GROUP_CONTRACT_ADDRESS=n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr
MULTISIG_CONTRACT_ADDRESS=n1txayqfz5g9qww3rlflpg025xd26m9payz96u54x4fe3s2ktz39xqk67gzx
COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mpxjsztfpvx
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
+2 -2
View File
@@ -37,7 +37,7 @@ nym-config = { path = "../common/config" }
nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" }
pretty_env_logger = "0.4"
refinery = { version = "0.8.7", features = ["rusqlite"], optional = true }
reqwest = { version = "0.11.22", default_features = false, features = ["rustls-tls", "json"] }
reqwest = { version = "0.12.4", default_features = false, features = ["rustls-tls", "json"] }
# Rocksdb kills compilation times and we're not currently using it. The reason
# we comment it out is that rust-analyzer runs with --all-features
#rocksdb = { version = "0.21.0", optional = true }
@@ -46,7 +46,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0.149"
serde_json = "1.0.91"
thiserror = { workspace = true }
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
tokio-tungstenite = { workspace = true }
tokio-util = { workspace = true, features = ["full"] }
toml = "0.7.0"
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals"]
}
+36
View File
@@ -0,0 +1,36 @@
# 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
+36
View File
@@ -0,0 +1,36 @@
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.
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import { Navbar } from './components/Nav/Navbar'
import { Providers } from './providers'
const App = ({ children }: { children: React.ReactNode }) => (
<Providers>
<Navbar>{children}</Navbar>
</Providers>
)
export { App }
+34
View File
@@ -0,0 +1,34 @@
// 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 BIG_DIPPER = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
// specific API routes
export const OVERVIEW_API = `${API_BASE_URL}/overview`;
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`;
export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`;
export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`;
export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
// 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.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox';
export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || '';
+173
View File
@@ -0,0 +1,173 @@
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';
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
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>
);
@@ -0,0 +1,38 @@
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>
)
@@ -0,0 +1,30 @@
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>
)
}
@@ -0,0 +1,83 @@
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>
);
};
@@ -0,0 +1,39 @@
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>
)
}
@@ -0,0 +1,191 @@
'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>
)
}
@@ -0,0 +1,95 @@
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>
)
}
@@ -0,0 +1,28 @@
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>
);
@@ -0,0 +1,18 @@
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>
);
@@ -0,0 +1,6 @@
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} />;
@@ -0,0 +1,32 @@
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>
);
@@ -0,0 +1,152 @@
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>
)
}
@@ -0,0 +1,10 @@
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';
@@ -0,0 +1,21 @@
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%' };
};
@@ -0,0 +1,145 @@
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'
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>
)
}
@@ -0,0 +1,193 @@
'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>
</>
)
}
@@ -0,0 +1,34 @@
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;
@@ -0,0 +1,69 @@
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),
});
+56
View File
@@ -0,0 +1,56 @@
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>
)
}
@@ -0,0 +1,52 @@
import { GatewayResponse, GatewayBond, GatewayReportResponse } 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: 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),
};
}
@@ -0,0 +1,51 @@
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>
)
}
+28
View File
@@ -0,0 +1,28 @@
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;
};
@@ -0,0 +1,213 @@
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&nbsp;&nbsp;
</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>
)
}
@@ -0,0 +1,114 @@
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>
)
}
@@ -0,0 +1,51 @@
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",
},
];
@@ -0,0 +1,31 @@
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,
};
@@ -0,0 +1,38 @@
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>
);
};
@@ -0,0 +1,107 @@
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',
};
@@ -0,0 +1,57 @@
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,
},
};
};
@@ -0,0 +1,47 @@
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>
)
}
@@ -0,0 +1,91 @@
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>
)
}
@@ -0,0 +1,3 @@
export { DelegatorsInfoTable } from './Table';
export { EconomicsInfoColumns } from './Columns';
export { EconomicsInfoRows } from './Rows';
@@ -0,0 +1,20 @@
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 };
@@ -0,0 +1,36 @@
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>
)
}
@@ -0,0 +1,83 @@
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>
)
}
@@ -0,0 +1,3 @@
export * from './Status';
export * from './StatusDropdown';
export * from './mappings';
@@ -0,0 +1,57 @@
/* 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,
};
}
@@ -0,0 +1,373 @@
'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 { DarkLightSwitchDesktop } from '@/app/components/Switch'
import { Footer } from '@/app/components/Footer'
import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet'
import { usePathname, useRouter } from 'next/navigation'
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
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 }}
>
{children}
<Footer />
</Box>
</Box>
)
}

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