Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4621978a54 | |||
| 93a1857c6b | |||
| c6942769fc | |||
| 33118ad648 | |||
| 1cd489034f | |||
| 6c731a2f06 | |||
| 5dda372437 | |||
| edf9c0f7b5 | |||
| c371e6c4bc | |||
| 4198cd81f3 | |||
| bbf57482fc | |||
| e59a444074 |
@@ -102,18 +102,6 @@ jobs:
|
||||
nym-wallet/target/release/bundle/dmg/*.dmg
|
||||
nym-wallet/target/release/bundle/macos/*.app.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/macos/nym-wallet.app.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
|
||||
@@ -77,18 +77,6 @@ jobs:
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
|
||||
@@ -97,18 +97,6 @@ jobs:
|
||||
nym-wallet/target/release/bundle/msi/*.msi
|
||||
nym-wallet/target/release/bundle/msi/*.msi.zip*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
|
||||
@@ -4,16 +4,6 @@ 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
+3144
-1299
File diff suppressed because it is too large
Load Diff
+5
-7
@@ -160,8 +160,7 @@ license = "Apache-2.0"
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.71"
|
||||
async-trait = "0.1.68"
|
||||
axum = "0.7.5"
|
||||
axum-extra = "0.9.3"
|
||||
axum = "0.6.20"
|
||||
base64 = "0.21.4"
|
||||
bs58 = "0.5.0"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
@@ -172,16 +171,15 @@ dotenvy = "0.15.6"
|
||||
futures = "0.3.28"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
headers = "0.4.0"
|
||||
humantime-serde = "1.1.1"
|
||||
hyper = "1.3.1"
|
||||
hyper = "0.14.27"
|
||||
k256 = "0.13"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
once_cell = "1.7.2"
|
||||
parking_lot = "0.12.1"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
reqwest = { version = "0.11.22", default-features = false }
|
||||
schemars = "0.8.1"
|
||||
serde = "1.0.152"
|
||||
serde_json = "1.0.91"
|
||||
@@ -195,8 +193,8 @@ tokio-tungstenite = { version = "0.20.1" }
|
||||
tracing = "0.1.37"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
ts-rs = "7.0.0"
|
||||
utoipa = "4.2.0"
|
||||
utoipa-swagger-ui = "6.0.0"
|
||||
utoipa = "3.5.0"
|
||||
utoipa-swagger-ui = "3.1.5"
|
||||
url = "2.4"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "nym-client-core"
|
||||
version = "1.1.15"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.66"
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -25,6 +25,7 @@ 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 }
|
||||
@@ -47,7 +48,7 @@ nym-validator-client = { path = "../client-libs/validator-client", default-featu
|
||||
nym-task = { path = "../task" }
|
||||
nym-credential-storage = { path = "../credential-storage" }
|
||||
nym-network-defaults = { path = "../network-defaults" }
|
||||
nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] }
|
||||
nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"]}
|
||||
nym-client-core-surb-storage = { path = "./surb-storage" }
|
||||
nym-client-core-gateways-storage = { path = "./gateways-storage" }
|
||||
|
||||
@@ -73,17 +74,8 @@ workspace = true
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
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
|
||||
version = "0.20.1"
|
||||
features = ["rustls-tls-native-roots"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures]
|
||||
workspace = true
|
||||
|
||||
@@ -8,12 +8,3 @@ use thiserror::Error;
|
||||
pub struct ConfigUpgradeFailure {
|
||||
pub current_version: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InvalidTrafficModeFailure {
|
||||
#[error("attempted to set medium toggle traffic mode with fast mode flag")]
|
||||
MediumToggleWithFastMode,
|
||||
|
||||
#[error("attempted to set medium toggle traffic mode with no cover flag")]
|
||||
MediumToggleWithNoCover,
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 6
|
||||
// 24 hours
|
||||
const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
use crate::error::InvalidTrafficModeFailure;
|
||||
pub use nym_country_group::CountryGroup;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
@@ -128,56 +127,6 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: this should be refactored properly
|
||||
// as of 12.09.23 the below is true (not sure how this comment will rot in the future)
|
||||
// medium_toggle:
|
||||
// - sets secondary packet size to 16kb
|
||||
// - disables poisson distribution of the main traffic stream
|
||||
// - sets the cover traffic stream to 1 packet / 5s (on average)
|
||||
// - disables per hop delay
|
||||
//
|
||||
// fastmode (to be renamed to `fast-poisson`):
|
||||
// - sets average per hop delay to 10ms
|
||||
// - sets the cover traffic stream to 1 packet / 2000s (on average); for all intents and purposes it disables the stream
|
||||
// - sets the poisson distribution of the main traffic stream to 4ms, i.e. 250 packets / s on average
|
||||
//
|
||||
// no_cover:
|
||||
// - disables poisson distribution of the main traffic stream
|
||||
// - disables the secondary cover traffic stream
|
||||
#[doc(hidden)]
|
||||
pub fn try_apply_traffic_modes(
|
||||
&mut self,
|
||||
disable_poisson_process: bool,
|
||||
medium_toggle: bool,
|
||||
fast_mode: bool,
|
||||
no_cover: bool,
|
||||
) -> Result<(), InvalidTrafficModeFailure> {
|
||||
if disable_poisson_process {
|
||||
self.set_no_poisson_process()
|
||||
}
|
||||
|
||||
if medium_toggle {
|
||||
if fast_mode {
|
||||
return Err(InvalidTrafficModeFailure::MediumToggleWithFastMode);
|
||||
}
|
||||
if no_cover {
|
||||
return Err(InvalidTrafficModeFailure::MediumToggleWithNoCover);
|
||||
}
|
||||
|
||||
self.set_experimental_medium_toggle();
|
||||
}
|
||||
|
||||
if fast_mode {
|
||||
self.set_high_default_traffic_volume()
|
||||
}
|
||||
|
||||
if no_cover {
|
||||
self.set_no_cover_traffic();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_high_default_traffic_volume(&mut self) {
|
||||
self.debug.traffic.average_packet_delay = Duration::from_millis(10);
|
||||
// basically don't really send cover messages
|
||||
@@ -187,15 +136,6 @@ impl Config {
|
||||
self.debug.traffic.message_sending_average_delay = Duration::from_millis(4);
|
||||
}
|
||||
|
||||
/// Enable medium mixnet traffic, for experiments only.
|
||||
/// This includes things like disabling cover traffic, no per hop delays, etc.
|
||||
#[doc(hidden)]
|
||||
pub fn set_experimental_medium_toggle(&mut self) {
|
||||
self.set_no_cover_traffic_with_keepalive();
|
||||
self.set_no_per_hop_delays();
|
||||
self.debug.traffic.secondary_packet_size = Some(PacketSize::ExtendedPacket16);
|
||||
}
|
||||
|
||||
pub fn with_disabled_poisson_process(mut self, disabled: bool) -> Self {
|
||||
if disabled {
|
||||
self.set_no_poisson_process()
|
||||
|
||||
@@ -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, identity};
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_gateway_client::{
|
||||
AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter,
|
||||
};
|
||||
@@ -670,7 +670,6 @@ 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.
|
||||
@@ -793,7 +792,6 @@ where
|
||||
|
||||
Ok(BaseClient {
|
||||
address: self_address,
|
||||
identity_keys,
|
||||
client_input: ClientInputStatus::AwaitingProducer {
|
||||
client_input: ClientInput {
|
||||
connection_command_sender: client_connection_tx,
|
||||
@@ -818,7 +816,6 @@ 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,
|
||||
|
||||
@@ -48,7 +48,10 @@ features = ["net", "sync", "time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
workspace = true
|
||||
features = ["rustls-tls-webpki-roots"]
|
||||
# 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"]
|
||||
|
||||
# wasm-only dependencies
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
|
||||
|
||||
@@ -442,7 +442,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
debug_assert!(self.connection.is_available());
|
||||
log::debug!("Registering gateway");
|
||||
log::trace!("Registering gateway");
|
||||
|
||||
// it's fine to instantiate it here as it's only used once (during authentication or registration)
|
||||
// and putting it into the GatewayClient struct would be a hassle
|
||||
@@ -494,7 +494,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
log::debug!("Authenticating with gateway");
|
||||
|
||||
// it's fine to instantiate it here as it's only used once (during authentication or registration)
|
||||
// and putting it into the GatewayClient struct would be a hassle
|
||||
@@ -530,7 +529,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
self.authenticated = status;
|
||||
self.bandwidth_remaining = bandwidth_remaining;
|
||||
self.negotiated_protocol = protocol_version;
|
||||
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
|
||||
Ok(())
|
||||
}
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
@@ -543,11 +541,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
&mut self,
|
||||
) -> Result<Arc<SharedKeys>, GatewayClientError> {
|
||||
if self.authenticated {
|
||||
debug!("Already authenticated");
|
||||
return if let Some(shared_key) = &self.shared_key {
|
||||
Ok(Arc::clone(shared_key))
|
||||
} else {
|
||||
Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey)
|
||||
Err(GatewayClientError::AuthenticationFailure)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,9 +71,6 @@ pub enum GatewayClientError {
|
||||
#[error("Authentication failure")]
|
||||
AuthenticationFailure,
|
||||
|
||||
#[error("Authentication failure with preexisting shared key")]
|
||||
AuthenticationFailureWithPreexistingSharedKey,
|
||||
|
||||
#[error("Timed out")]
|
||||
Timeout,
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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 }
|
||||
@@ -66,14 +67,6 @@ 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"] }
|
||||
|
||||
@@ -157,7 +157,7 @@ async fn fetch_delegation_data(
|
||||
// If a pending undelegate tx is found, remove it from delegation map
|
||||
Undelegate { owner, mix_id, .. } => {
|
||||
if owner == address.as_ref()
|
||||
&& existing_delegation_map.contains_key(&mix_id.to_string())
|
||||
&& existing_delegation_map.get(&mix_id.to_string()).is_some()
|
||||
{
|
||||
existing_delegation_map.remove(&mix_id.to_string());
|
||||
}
|
||||
|
||||
@@ -328,8 +328,4 @@ impl EpochState {
|
||||
pub fn is_dealing_exchange(&self) -> bool {
|
||||
matches!(self, EpochState::DealingExchange { .. })
|
||||
}
|
||||
|
||||
pub fn is_waiting_initialisation(&self) -> bool {
|
||||
matches!(self, EpochState::WaitingInitialisation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,4 @@ 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,21 +69,9 @@ impl Storage for PersistentStorage {
|
||||
bandwidth_credential.credential_data,
|
||||
bandwidth_credential.epoch_id,
|
||||
)
|
||||
.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()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_next_unspent_credential(
|
||||
|
||||
@@ -8,11 +8,11 @@ use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use nym_coconut::{
|
||||
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,
|
||||
aggregate_signature_shares, 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";
|
||||
|
||||
@@ -12,8 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||
use time::{Duration, OffsetDateTime, Time};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
|
||||
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks
|
||||
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
|
||||
|
||||
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub struct FreePassIssuedData {
|
||||
@@ -78,9 +77,9 @@ impl FreePassIssuanceData {
|
||||
}
|
||||
|
||||
pub fn default_expiry_date() -> OffsetDateTime {
|
||||
// set it to the furthest midnight in the future such as it's no more than a week away,
|
||||
// set it to 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() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
|
||||
(OffsetDateTime::now_utc() + MAX_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
|
||||
}
|
||||
|
||||
pub fn expiry_date_attribute(&self) -> &Attribute {
|
||||
|
||||
@@ -10,19 +10,18 @@ use crate::coconut::bandwidth::{
|
||||
use crate::coconut::utils::scalar_serde_helper;
|
||||
use crate::error::Error;
|
||||
use nym_credentials_interface::{
|
||||
aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar,
|
||||
prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters,
|
||||
PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey,
|
||||
aggregate_signature_shares, 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;
|
||||
use nym_validator_client::nyxd::{Coin, Hash};
|
||||
use nym_validator_client::signing::AccountData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub use nym_validator_client::nyxd::{Coin, Hash};
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
pub enum BandwidthCredentialIssuanceDataVariant {
|
||||
Voucher(BandwidthVoucherIssuanceData),
|
||||
@@ -266,13 +265,6 @@ 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,
|
||||
@@ -287,7 +279,7 @@ impl IssuanceBandwidthCredential {
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
aggregate_signature_shares_and_verify(params, verification_key, &attributes, shares)
|
||||
aggregate_signature_shares(params, verification_key, &attributes, shares)
|
||||
.map_err(Error::SignatureAggregationError)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::coconut::utils::scalar_serde_helper;
|
||||
use crate::error::Error;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_credentials_interface::{
|
||||
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, CredentialType, PublicAttribute,
|
||||
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, PublicAttribute,
|
||||
};
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_validator_client::nyxd::{Coin, Hash};
|
||||
@@ -123,10 +123,6 @@ impl BandwidthVoucherIssuanceData {
|
||||
&self.value_prehashed
|
||||
}
|
||||
|
||||
pub fn typ() -> CredentialType {
|
||||
CredentialType::Voucher
|
||||
}
|
||||
|
||||
pub fn tx_hash(&self) -> Hash {
|
||||
self.deposit_tx_hash
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
|
||||
|
||||
pub const COCONUT_BANDWIDTH_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 GROUP_CONTRACT_ADDRESS: &str = "";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "";
|
||||
pub const EPHEMERA_CONTRACT_ADDRESS: &str = "";
|
||||
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
@@ -6,10 +6,10 @@ use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ff::Field;
|
||||
use group::{Curve, Group};
|
||||
use nym_coconut::{
|
||||
aggregate_signature_shares_and_verify, aggregate_verification_keys, blind_sign,
|
||||
prepare_blind_sign, prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen,
|
||||
verify_credential, verify_partial_blind_signature, Attribute, BlindedSignature, Parameters,
|
||||
Signature, SignatureShare, VerificationKey,
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign,
|
||||
prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen, verify_credential,
|
||||
verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, Signature,
|
||||
SignatureShare, VerificationKey,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use std::ops::Neg;
|
||||
@@ -99,7 +99,7 @@ fn unblind_and_aggregate(
|
||||
let mut attributes = vec![];
|
||||
attributes.extend_from_slice(private_attributes);
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
aggregate_signature_shares_and_verify(
|
||||
aggregate_signature_shares(
|
||||
params,
|
||||
verification_key,
|
||||
&attributes,
|
||||
|
||||
@@ -4,18 +4,14 @@
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
pub use bls12_381::Scalar;
|
||||
pub use elgamal::elgamal_keygen;
|
||||
pub use elgamal::ElGamalKeyPair;
|
||||
pub use elgamal::PublicKey;
|
||||
pub use error::CoconutError;
|
||||
pub use scheme::aggregation::aggregate_key_shares;
|
||||
pub use scheme::aggregation::aggregate_signature_shares;
|
||||
pub use scheme::aggregation::aggregate_signature_shares_and_verify;
|
||||
pub use scheme::aggregation::aggregate_verification_keys;
|
||||
pub use scheme::issuance::blind_sign;
|
||||
pub use scheme::issuance::prepare_blind_sign;
|
||||
pub use scheme::issuance::sign;
|
||||
pub use scheme::issuance::verify_partial_blind_signature;
|
||||
pub use scheme::issuance::BlindSignRequest;
|
||||
pub use scheme::keygen::keygen;
|
||||
@@ -23,19 +19,16 @@ pub use scheme::keygen::ttp_keygen;
|
||||
pub use scheme::keygen::KeyPair;
|
||||
pub use scheme::keygen::SecretKey;
|
||||
pub use scheme::keygen::VerificationKey;
|
||||
pub use scheme::keygen::VerificationKeyShare;
|
||||
pub use scheme::setup::setup;
|
||||
pub use scheme::setup::Parameters;
|
||||
pub use scheme::verification::check_vk_pairing;
|
||||
pub use scheme::verification::prove_bandwidth_credential;
|
||||
pub use scheme::verification::verify;
|
||||
pub use scheme::verification::verify_credential;
|
||||
pub use scheme::verification::BlindedSerialNumber;
|
||||
pub use scheme::verification::VerifyCredentialRequest;
|
||||
pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
pub use scheme::SignatureShare;
|
||||
pub use scheme::SignerIndex;
|
||||
pub use traits::Base58;
|
||||
pub use traits::Bytable;
|
||||
pub use utils::hash_to_scalar;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
|
||||
use crate::utils::perform_lagrangian_interpolation_at_origin;
|
||||
use crate::{Attribute, Parameters, VerificationKeyShare};
|
||||
use crate::{Attribute, Parameters};
|
||||
|
||||
pub(crate) trait Aggregatable: Sized {
|
||||
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
|
||||
@@ -80,23 +80,7 @@ pub fn aggregate_verification_keys(
|
||||
Aggregatable::aggregate(keys, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_key_shares(shares: &[VerificationKeyShare]) -> Result<VerificationKey> {
|
||||
let (keys, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (share.key.clone(), share.index))
|
||||
.unzip();
|
||||
|
||||
aggregate_verification_keys(&keys, Some(&indices))
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures(
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
Aggregatable::aggregate(signatures, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures_and_verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[&Attribute],
|
||||
@@ -104,7 +88,11 @@ pub fn aggregate_signatures_and_verify(
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
// aggregate the signature
|
||||
let signature = aggregate_signatures(signatures, indices)?;
|
||||
|
||||
let signature = match Aggregatable::aggregate(signatures, indices) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
// Verify the signature
|
||||
let alpha = verification_key.alpha;
|
||||
@@ -128,16 +116,7 @@ pub fn aggregate_signatures_and_verify(
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares(shares: &[SignatureShare]) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures(&signatures, Some(&indices))
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares_and_verify(
|
||||
pub fn aggregate_signature_shares(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[&Attribute],
|
||||
@@ -148,7 +127,7 @@ pub fn aggregate_signature_shares_and_verify(
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures_and_verify(
|
||||
aggregate_signatures(
|
||||
params,
|
||||
verification_key,
|
||||
attributes,
|
||||
@@ -231,7 +210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_works_for_any_subset_of_signatures() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
@@ -248,12 +227,12 @@ mod tests {
|
||||
|
||||
let sigs = sks
|
||||
.iter()
|
||||
.map(|sk| sign(¶ms, sk, &attributes).unwrap())
|
||||
.map(|sk| sign(&mut params, sk, &attributes).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// aggregating (any) threshold works
|
||||
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_sig1 = aggregate_signatures_and_verify(
|
||||
let aggr_sig1 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
@@ -263,7 +242,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
let aggr_sig2 = aggregate_signatures_and_verify(
|
||||
let aggr_sig2 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
@@ -279,7 +258,7 @@ mod tests {
|
||||
|
||||
// aggregating threshold+1 works
|
||||
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
let aggr_more = aggregate_signatures_and_verify(
|
||||
let aggr_more = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_more,
|
||||
&attributes,
|
||||
@@ -291,7 +270,7 @@ mod tests {
|
||||
|
||||
// aggregating all
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
let aggr_all = aggregate_signatures_and_verify(
|
||||
let aggr_all = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
@@ -303,7 +282,7 @@ mod tests {
|
||||
|
||||
// not taking enough points (threshold was 3) should fail
|
||||
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_not_enough = aggregate_signatures_and_verify(
|
||||
let aggr_not_enough = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_not_enough,
|
||||
&attributes,
|
||||
@@ -315,7 +294,7 @@ mod tests {
|
||||
|
||||
// taking wrong index should fail
|
||||
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_bad,
|
||||
&attributes,
|
||||
@@ -351,14 +330,9 @@ mod tests {
|
||||
.unzip();
|
||||
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
None
|
||||
)
|
||||
.is_err());
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, None).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -378,15 +352,11 @@ mod tests {
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[])
|
||||
)
|
||||
.is_err());
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, Some(&[]))
|
||||
.is_err()
|
||||
);
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
@@ -413,7 +383,7 @@ mod tests {
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
|
||||
@@ -13,8 +13,9 @@ use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::BlindedSignature;
|
||||
use crate::scheme::SecretKey;
|
||||
use crate::Attribute;
|
||||
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
|
||||
#[cfg(test)]
|
||||
use crate::Signature;
|
||||
|
||||
// TODO: possibly completely remove those two functions.
|
||||
// They only exist to have a simpler and smaller code snippets to test
|
||||
// basic functionalities.
|
||||
@@ -157,10 +158,6 @@ impl BlindSignRequest {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_commitment_hash(&self, public_attributes: &[&Attribute]) -> bool {
|
||||
self.commitment_hash == compute_hash(self.commitment, public_attributes)
|
||||
}
|
||||
|
||||
pub fn get_commitment_hash(&self) -> G1Projective {
|
||||
self.commitment_hash
|
||||
}
|
||||
@@ -429,9 +426,9 @@ pub fn verify_partial_blind_signature(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
|
||||
#[cfg(test)]
|
||||
pub fn sign(
|
||||
params: &Parameters,
|
||||
params: &mut Parameters,
|
||||
secret_key: &SecretKey,
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<Signature> {
|
||||
|
||||
@@ -151,6 +151,10 @@ impl Base58 for SecretKey {}
|
||||
// TODO: perhaps change points to affine representation
|
||||
// to make verification slightly more efficient?
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct VerificationKey {
|
||||
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
|
||||
pub(crate) alpha: G2Projective,
|
||||
@@ -407,23 +411,12 @@ impl Bytable for VerificationKey {
|
||||
|
||||
impl Base58 for VerificationKey {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerificationKeyShare {
|
||||
pub key: VerificationKey,
|
||||
pub index: SignerIndex,
|
||||
}
|
||||
|
||||
impl From<(VerificationKey, SignerIndex)> for VerificationKeyShare {
|
||||
fn from(value: (VerificationKey, SignerIndex)) -> Self {
|
||||
VerificationKeyShare {
|
||||
key: value.0,
|
||||
index: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct KeyPair {
|
||||
secret_key: SecretKey,
|
||||
verification_key: VerificationKey,
|
||||
@@ -432,12 +425,6 @@ pub struct KeyPair {
|
||||
pub index: Option<SignerIndex>,
|
||||
}
|
||||
|
||||
impl From<KeyPair> for (SecretKey, VerificationKey) {
|
||||
fn from(value: KeyPair) -> Self {
|
||||
(value.secret_key, value.verification_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKeyPair for KeyPair {
|
||||
type PrivatePemKey = SecretKey;
|
||||
type PublicPemKey = VerificationKey;
|
||||
@@ -474,13 +461,6 @@ impl KeyPair {
|
||||
&self.verification_key
|
||||
}
|
||||
|
||||
pub fn to_verification_key_share(&self) -> Option<VerificationKeyShare> {
|
||||
self.index.map(|index| VerificationKeyShare {
|
||||
key: self.verification_key.clone(),
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
self.to_byte_vec()
|
||||
|
||||
@@ -70,11 +70,6 @@ impl Signature {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn randomise_simple(&self, params: &Parameters) -> Signature {
|
||||
let r = params.random_scalar();
|
||||
Signature(self.0 * r, self.1 * r)
|
||||
}
|
||||
|
||||
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
|
||||
let r = params.random_scalar();
|
||||
let r_prime = params.random_scalar();
|
||||
@@ -196,7 +191,7 @@ impl BlindedSignature {
|
||||
&self,
|
||||
partial_verification_key: &VerificationKey,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Signature {
|
||||
) -> Result<Signature> {
|
||||
// parse the signature
|
||||
let h = &self.0;
|
||||
let c = &self.1;
|
||||
@@ -209,7 +204,7 @@ impl BlindedSignature {
|
||||
|
||||
let unblinded_c = c - blinding_removers;
|
||||
|
||||
Signature(*h, unblinded_c)
|
||||
Ok(Signature(*h, unblinded_c))
|
||||
}
|
||||
|
||||
pub fn unblind_and_verify(
|
||||
@@ -221,7 +216,7 @@ impl BlindedSignature {
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Result<Signature> {
|
||||
let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings);
|
||||
let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings)?;
|
||||
unblinded.verify(
|
||||
params,
|
||||
partial_verification_key,
|
||||
@@ -245,7 +240,6 @@ impl BlindedSignature {
|
||||
}
|
||||
|
||||
// perhaps this should take signature by reference? we'll see how it goes
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignatureShare {
|
||||
signature: Signature,
|
||||
index: SignerIndex,
|
||||
@@ -282,9 +276,7 @@ impl SignatureShare {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hash_to_scalar;
|
||||
use crate::scheme::aggregation::{
|
||||
aggregate_signatures_and_verify, aggregate_verification_keys,
|
||||
};
|
||||
use crate::scheme::aggregation::{aggregate_signatures, aggregate_verification_keys};
|
||||
use crate::scheme::issuance::{blind_sign, compute_hash, prepare_blind_sign, sign};
|
||||
use crate::scheme::keygen::{keygen, ttp_keygen};
|
||||
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
|
||||
@@ -426,13 +418,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_attributes() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
let sig1 = sign(¶ms, keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(¶ms, keypair2.secret_key(), &attributes).unwrap();
|
||||
let sig1 = sign(&mut params, keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(&mut params, keypair2.secret_key(), &attributes).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
@@ -576,14 +568,9 @@ mod tests {
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_sig = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&attributes,
|
||||
&sigs[..2],
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
@@ -603,14 +590,9 @@ mod tests {
|
||||
|
||||
// taking different subset of keys and credentials
|
||||
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
|
||||
let aggr_sig = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&attributes,
|
||||
&sigs[1..],
|
||||
Some(&[2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::error::{CoconutError, Result};
|
||||
use crate::utils::hash_g1;
|
||||
|
||||
/// System-wide parameters used for the protocol
|
||||
#[derive(Clone)]
|
||||
pub struct Parameters {
|
||||
/// Generator of the G1 group
|
||||
g1: G1Affine,
|
||||
|
||||
@@ -288,6 +288,7 @@ pub fn verify_credential(
|
||||
}
|
||||
|
||||
// Used in tests only
|
||||
#[cfg(test)]
|
||||
pub fn verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
|
||||
@@ -75,12 +75,8 @@ pub fn theta_from_keys_and_attributes(
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
|
||||
// Randomize credentials and generate any cryptographic material to verify them
|
||||
let signature = aggregate_signature_shares_and_verify(
|
||||
params,
|
||||
&verification_key,
|
||||
&attributes,
|
||||
&signature_shares,
|
||||
)?;
|
||||
let signature =
|
||||
aggregate_signature_shares(params, &verification_key, &attributes, &signature_shares)?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
let theta = prove_bandwidth_credential(
|
||||
|
||||
@@ -16,9 +16,7 @@ const_format = "0.2.32"
|
||||
cosmrs.workspace = true
|
||||
eyre = "0.6.9"
|
||||
futures.workspace = true
|
||||
humantime = "2.1.0"
|
||||
sha2 = "0.10.8"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] }
|
||||
tendermint.workspace = true
|
||||
tendermint-rpc = { workspace = true, features = ["websocket-client", "http-client"] }
|
||||
@@ -26,13 +24,13 @@ thiserror.workspace = true
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tokio-util = { version = "0.7.10", features = ["rt"] }
|
||||
tokio-util = { version = "0.7.10", features = ["rt"]}
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
|
||||
# TEMP
|
||||
#nym-bin-common = { path = "../bin-common", features = ["basic_tracing"]}
|
||||
nym-bin-common = { path = "../bin-common", features = ["basic_tracing"]}
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Nyxd Scraper
|
||||
|
||||
## Pruning
|
||||
|
||||
Similarly to cosmos-sdk, we incorporate pruning into our (scraped) chain data. We attempt to follow their strategies as
|
||||
closely as possible for convenience's sake. Therefore, the following are available:
|
||||
|
||||
### Strategies
|
||||
|
||||
The strategies are configured in `config.toml`, with the format `pruning = "<strategy>"` where the options are:
|
||||
|
||||
* `default`: only the last 362,880 states(approximately 3.5 weeks worth of state) are kept; pruning at 10 block
|
||||
intervals
|
||||
* `nothing`: all historic states will be saved, nothing will be deleted (i.e. archiving node)
|
||||
* `everything`: 2 latest states will be kept; pruning at 10 block intervals.
|
||||
* `custom`: allow pruning options to be manually specified through `pruning.keep_recent`, and `pruning.interval`
|
||||
|
||||
### Custom Pruning
|
||||
|
||||
These are applied if and only if the pruning strategy is `custom`:
|
||||
|
||||
* `pruning.keep_recent`: N means to keep all of the last N blocks
|
||||
* `pruning.interval`: N means to delete old block data from disk every Nth block.
|
||||
@@ -8,7 +8,6 @@ use crate::error::ScraperError;
|
||||
use crate::modules::{BlockModule, MsgModule, TxModule};
|
||||
use crate::rpc_client::RpcClient;
|
||||
use crate::storage::{persist_block, ScraperStorage};
|
||||
use crate::PruningOptions;
|
||||
use futures::StreamExt;
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use std::ops::{Add, Range};
|
||||
@@ -19,10 +18,9 @@ use tokio::sync::Notify;
|
||||
use tokio::time::{interval_at, Instant};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
mod helpers;
|
||||
pub(crate) mod pruning;
|
||||
pub(crate) mod types;
|
||||
|
||||
const MISSING_BLOCKS_CHECK_INTERVAL: Duration = Duration::from_secs(30);
|
||||
@@ -42,11 +40,9 @@ impl PendingSync {
|
||||
}
|
||||
|
||||
pub struct BlockProcessor {
|
||||
pruning_options: PruningOptions,
|
||||
cancel: CancellationToken,
|
||||
synced: Arc<Notify>,
|
||||
last_processed_height: u32,
|
||||
last_pruned_height: u32,
|
||||
last_processed_at: Instant,
|
||||
pending_sync: PendingSync,
|
||||
queued_blocks: BTreeMap<u32, BlockToProcess>,
|
||||
@@ -66,7 +62,6 @@ pub struct BlockProcessor {
|
||||
|
||||
impl BlockProcessor {
|
||||
pub async fn new(
|
||||
pruning_options: PruningOptions,
|
||||
cancel: CancellationToken,
|
||||
synced: Arc<Notify>,
|
||||
incoming: UnboundedReceiver<BlockToProcess>,
|
||||
@@ -75,17 +70,11 @@ impl BlockProcessor {
|
||||
rpc_client: RpcClient,
|
||||
) -> Result<Self, ScraperError> {
|
||||
let last_processed = storage.get_last_processed_height().await?;
|
||||
let last_processed_height = last_processed.try_into().unwrap_or_default();
|
||||
|
||||
let last_pruned = storage.get_pruned_height().await?;
|
||||
let last_pruned_height = last_pruned.try_into().unwrap_or_default();
|
||||
|
||||
Ok(BlockProcessor {
|
||||
pruning_options,
|
||||
cancel,
|
||||
synced,
|
||||
last_processed_height,
|
||||
last_pruned_height,
|
||||
last_processed_height: last_processed.try_into().unwrap_or_default(),
|
||||
last_processed_at: Instant::now(),
|
||||
pending_sync: Default::default(),
|
||||
queued_blocks: Default::default(),
|
||||
@@ -142,17 +131,12 @@ impl BlockProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
let commit_start = Instant::now();
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|source| ScraperError::StorageTxCommitFailure { source })?;
|
||||
crate::storage::log_db_operation_time("committing processing tx", commit_start);
|
||||
|
||||
self.last_processed_height = full_info.block.header.height.value() as u32;
|
||||
self.last_processed_at = Instant::now();
|
||||
if let Err(err) = self.maybe_prune_storage().await {
|
||||
error!("failed to prune the storage: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -226,61 +210,6 @@ impl BlockProcessor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn prune_storage(&mut self) -> Result<(), ScraperError> {
|
||||
let keep_recent = self.pruning_options.strategy_keep_recent();
|
||||
let last_to_keep = self.last_processed_height - keep_recent;
|
||||
|
||||
info!(
|
||||
keep_recent,
|
||||
oldest_to_keep = last_to_keep,
|
||||
"pruning the storage"
|
||||
);
|
||||
|
||||
let lowest: u32 = self
|
||||
.storage
|
||||
.lowest_block_height()
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
let to_prune = last_to_keep.saturating_sub(lowest);
|
||||
match to_prune {
|
||||
v if v > 1000 => warn!("approximately {v} blocks worth of data will be pruned"),
|
||||
v if v > 100 => info!("approximately {v} blocks worth of data will be pruned"),
|
||||
0 => trace!("no blocks to prune"),
|
||||
v => debug!("approximately {v} blocks worth of data will be pruned"),
|
||||
}
|
||||
|
||||
if to_prune == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.storage
|
||||
.prune_storage(last_to_keep, self.last_processed_height)
|
||||
.await?;
|
||||
|
||||
self.last_pruned_height = self.last_processed_height;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_prune_storage(&mut self) -> Result<(), ScraperError> {
|
||||
debug!("checking for storage pruning");
|
||||
|
||||
if self.pruning_options.strategy.is_nothing() {
|
||||
trace!("the current pruning strategy is 'nothing'");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let interval = self.pruning_options.strategy_interval();
|
||||
if self.last_pruned_height + interval <= self.last_processed_height {
|
||||
self.prune_storage().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn next_incoming(&mut self, block: BlockToProcess) {
|
||||
let height = block.height;
|
||||
|
||||
@@ -350,8 +279,6 @@ impl BlockProcessor {
|
||||
async fn startup_resync(&mut self) -> Result<(), ScraperError> {
|
||||
assert!(self.pending_sync.is_empty());
|
||||
|
||||
self.maybe_prune_storage().await?;
|
||||
|
||||
let latest_block = self.rpc_client.current_block_height().await? as u32;
|
||||
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
|
||||
let request_range = self.last_processed_height + 1..latest_block + 1;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::ScraperError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const DEFAULT_PRUNING_KEEP_RECENT: u32 = 362880;
|
||||
pub const DEFAULT_PRUNING_INTERVAL: u32 = 10;
|
||||
pub const EVERYTHING_PRUNING_KEEP_RECENT: u32 = 2;
|
||||
pub const EVERYTHING_PRUNING_INTERVAL: u32 = 10;
|
||||
|
||||
/// We follow cosmos-sdk pruning strategies for convenience’s sake.
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PruningStrategy {
|
||||
/// 'Default' strategy defines a pruning strategy where the last 362880 heights are
|
||||
/// kept where to-be pruned heights are pruned at every 10th height.
|
||||
/// The last 362880 heights are kept(approximately 3.5 weeks worth of state) assuming the typical
|
||||
/// block time is 6s. If these values do not match the applications' requirements, use the "custom" option.
|
||||
#[default]
|
||||
Default,
|
||||
|
||||
/// 'Everything' strategy defines a pruning strategy where all committed heights are
|
||||
/// deleted, storing only the current height and last 2 states. To-be pruned heights are
|
||||
/// pruned at every 10th height.
|
||||
Everything,
|
||||
|
||||
/// 'Nothing' strategy defines a pruning strategy where all heights are kept on disk.
|
||||
Nothing,
|
||||
|
||||
/// 'Custom' strategy defines a pruning strategy where the user specifies the pruning.
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl PruningStrategy {
|
||||
pub fn is_custom(&self) -> bool {
|
||||
matches!(self, PruningStrategy::Custom)
|
||||
}
|
||||
|
||||
pub fn is_nothing(&self) -> bool {
|
||||
matches!(self, PruningStrategy::Nothing)
|
||||
}
|
||||
|
||||
pub fn is_everything(&self) -> bool {
|
||||
matches!(self, PruningStrategy::Everything)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct PruningOptions {
|
||||
/// keep_recent defines how many recent heights to keep on disk.
|
||||
pub keep_recent: u32,
|
||||
|
||||
/// interval defines the frequency of removing the pruned heights from the disk.
|
||||
pub interval: u32,
|
||||
|
||||
/// strategy defines the currently used kind of [PruningStrategy].
|
||||
pub strategy: PruningStrategy,
|
||||
}
|
||||
|
||||
impl PruningOptions {
|
||||
pub fn validate(&self) -> Result<(), ScraperError> {
|
||||
// if strategy is not set to custom, other options are meaningless since they won't be applied
|
||||
if !self.strategy.is_custom() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.interval == 0 {
|
||||
return Err(ScraperError::ZeroPruningInterval);
|
||||
}
|
||||
|
||||
if self.interval < EVERYTHING_PRUNING_INTERVAL {
|
||||
return Err(ScraperError::TooSmallPruningInterval {
|
||||
interval: self.interval,
|
||||
});
|
||||
}
|
||||
|
||||
if self.keep_recent < EVERYTHING_PRUNING_KEEP_RECENT {
|
||||
return Err(ScraperError::TooSmallKeepRecent {
|
||||
keep_recent: self.keep_recent,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn nothing() -> Self {
|
||||
PruningOptions {
|
||||
keep_recent: 0,
|
||||
interval: 0,
|
||||
strategy: PruningStrategy::Nothing,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn strategy_interval(&self) -> u32 {
|
||||
match self.strategy {
|
||||
PruningStrategy::Default => DEFAULT_PRUNING_INTERVAL,
|
||||
PruningStrategy::Everything => EVERYTHING_PRUNING_INTERVAL,
|
||||
PruningStrategy::Nothing => 0,
|
||||
PruningStrategy::Custom => self.interval,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn strategy_keep_recent(&self) -> u32 {
|
||||
match self.strategy {
|
||||
PruningStrategy::Default => DEFAULT_PRUNING_KEEP_RECENT,
|
||||
PruningStrategy::Everything => EVERYTHING_PRUNING_KEEP_RECENT,
|
||||
PruningStrategy::Nothing => 0,
|
||||
PruningStrategy::Custom => self.keep_recent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PruningOptions {
|
||||
fn default() -> Self {
|
||||
PruningOptions {
|
||||
keep_recent: DEFAULT_PRUNING_KEEP_RECENT,
|
||||
interval: DEFAULT_PRUNING_INTERVAL,
|
||||
strategy: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::pruning::{
|
||||
EVERYTHING_PRUNING_INTERVAL, EVERYTHING_PRUNING_KEEP_RECENT,
|
||||
};
|
||||
use tendermint::Hash;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc::error::SendError;
|
||||
@@ -125,15 +122,6 @@ pub enum ScraperError {
|
||||
"could not find validator information for {address}; the validator has signed a commit"
|
||||
)]
|
||||
MissingValidatorInfoCommitted { address: String },
|
||||
|
||||
#[error("pruning.interval must not be set to 0. If you want to disable pruning, select pruning.strategy = \"nothing\"")]
|
||||
ZeroPruningInterval,
|
||||
|
||||
#[error("pruning.interval must not be smaller than {}. got: {interval}. for most aggressive pruning, select pruning.strategy = \"everything\"", EVERYTHING_PRUNING_INTERVAL)]
|
||||
TooSmallPruningInterval { interval: u32 },
|
||||
|
||||
#[error("pruning.keep_recent must not be smaller than {}. got: {keep_recent}. for most aggressive pruning, select pruning.strategy = \"everything\"", EVERYTHING_PRUNING_KEEP_RECENT)]
|
||||
TooSmallKeepRecent { keep_recent: u32 },
|
||||
}
|
||||
|
||||
impl<T> From<SendError<T>> for ScraperError {
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(crate) mod rpc_client;
|
||||
pub(crate) mod scraper;
|
||||
pub mod storage;
|
||||
|
||||
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
|
||||
pub use modules::{BlockModule, MsgModule, TxModule};
|
||||
pub use scraper::{Config, NyxdScraper};
|
||||
pub use storage::models;
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::modules::{BlockModule, MsgModule, TxModule};
|
||||
use crate::rpc_client::RpcClient;
|
||||
use crate::scraper::subscriber::ChainSubscriber;
|
||||
use crate::storage::ScraperStorage;
|
||||
use crate::PruningOptions;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{channel, unbounded_channel};
|
||||
@@ -28,8 +27,6 @@ pub struct Config {
|
||||
pub rpc_url: Url,
|
||||
|
||||
pub database_path: PathBuf,
|
||||
|
||||
pub pruning_options: PruningOptions,
|
||||
}
|
||||
|
||||
pub struct NyxdScraperBuilder {
|
||||
@@ -57,7 +54,6 @@ impl NyxdScraperBuilder {
|
||||
processing_tx.clone(),
|
||||
);
|
||||
let mut block_processor = BlockProcessor::new(
|
||||
scraper.config.pruning_options,
|
||||
scraper.cancel_token.clone(),
|
||||
scraper.startup_sync.clone(),
|
||||
processing_rx,
|
||||
@@ -123,7 +119,6 @@ impl NyxdScraper {
|
||||
}
|
||||
|
||||
pub async fn new(config: Config) -> Result<Self, ScraperError> {
|
||||
config.pruning_options.validate()?;
|
||||
let storage = ScraperStorage::init(&config.database_path).await?;
|
||||
|
||||
Ok(NyxdScraper {
|
||||
@@ -165,7 +160,6 @@ impl NyxdScraper {
|
||||
processing_tx.clone(),
|
||||
);
|
||||
let block_processor = BlockProcessor::new(
|
||||
self.config.pruning_options,
|
||||
self.cancel_token.clone(),
|
||||
self.startup_sync.clone(),
|
||||
processing_rx,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::log_db_operation_time;
|
||||
use crate::storage::models::{CommitSignature, Validator};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -27,36 +25,10 @@ impl StorageManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_lowest_block(&self) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_lowest_block");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
FROM block
|
||||
ORDER BY height ASC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_lowest_block", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.height)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_first_block_height_after(
|
||||
&self,
|
||||
time: OffsetDateTime,
|
||||
) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_first_block_height_after");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
@@ -69,7 +41,6 @@ impl StorageManager {
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_first_block_height_after", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.height)
|
||||
@@ -82,9 +53,6 @@ impl StorageManager {
|
||||
&self,
|
||||
time: OffsetDateTime,
|
||||
) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_last_block_height_before");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
@@ -97,7 +65,6 @@ impl StorageManager {
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_last_block_height_before", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.height)
|
||||
@@ -112,9 +79,6 @@ impl StorageManager {
|
||||
start_height: i64,
|
||||
end_height: i64,
|
||||
) -> Result<i32, sqlx::Error> {
|
||||
trace!("get_signed_between");
|
||||
let start = Instant::now();
|
||||
|
||||
let count = sqlx::query!(
|
||||
r#"
|
||||
SELECT COUNT(*) as count FROM pre_commit
|
||||
@@ -130,7 +94,6 @@ impl StorageManager {
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?
|
||||
.count;
|
||||
log_db_operation_time("get_signed_between", start);
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
@@ -140,10 +103,7 @@ impl StorageManager {
|
||||
consensus_address: &str,
|
||||
height: i64,
|
||||
) -> Result<Option<CommitSignature>, sqlx::Error> {
|
||||
trace!("get_precommit");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as(
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT * FROM pre_commit
|
||||
WHERE validator_address = ?
|
||||
@@ -153,20 +113,14 @@ impl StorageManager {
|
||||
.bind(consensus_address)
|
||||
.bind(height)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_precommit", start);
|
||||
|
||||
Ok(res)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_block_validators(
|
||||
&self,
|
||||
height: i64,
|
||||
) -> Result<Vec<Validator>, sqlx::Error> {
|
||||
trace!("get_block_validators");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
sqlx::query_as!(
|
||||
Validator,
|
||||
r#"
|
||||
SELECT * FROM validator
|
||||
@@ -179,28 +133,16 @@ impl StorageManager {
|
||||
height
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_block_validators", start);
|
||||
|
||||
Ok(res)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_validators(&self) -> Result<Vec<Validator>, sqlx::Error> {
|
||||
trace!("get_validators");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as("SELECT * FROM validator")
|
||||
sqlx::query_as("SELECT * FROM validator")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_validators", start);
|
||||
|
||||
Ok(res)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_last_processed_height(&self) -> Result<i64, sqlx::Error> {
|
||||
trace!("get_last_processed_height");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT last_processed_height FROM metadata
|
||||
@@ -208,7 +150,6 @@ impl StorageManager {
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_last_processed_height", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.last_processed_height)
|
||||
@@ -216,27 +157,6 @@ impl StorageManager {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pruned_height(&self) -> Result<i64, sqlx::Error> {
|
||||
trace!("get_pruned_height");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT last_pruned_height FROM pruning
|
||||
"#
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
log_db_operation_time("get_pruned_height", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.last_pruned_height)
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make those generic over executor so that they could be performed over connection pool and a tx
|
||||
@@ -250,8 +170,7 @@ pub(crate) async fn insert_validator<'a, E>(
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("insert_validator");
|
||||
let start = Instant::now();
|
||||
trace!("insert validator");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -264,7 +183,6 @@ where
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_validator", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -282,8 +200,7 @@ pub(crate) async fn insert_block<'a, E>(
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("insert_block");
|
||||
let start = Instant::now();
|
||||
trace!("insert block");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -300,7 +217,6 @@ where
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_block", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -317,8 +233,7 @@ pub(crate) async fn insert_precommit<'a, E>(
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("insert_precommit");
|
||||
let start = Instant::now();
|
||||
trace!("insert precommit");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -334,7 +249,6 @@ where
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_precommit", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -356,8 +270,7 @@ pub(crate) async fn insert_transaction<'a, E>(
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("insert_transaction");
|
||||
let start = Instant::now();
|
||||
trace!("insert transaction");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -385,7 +298,6 @@ where
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_transaction", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -401,8 +313,7 @@ pub(crate) async fn insert_message<'a, E>(
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("insert_message");
|
||||
let start = Instant::now();
|
||||
trace!("insert message");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -419,7 +330,6 @@ where
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_message", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -433,100 +343,10 @@ where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("update_last_processed");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("UPDATE metadata SET last_processed_height = ?", height)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("update_last_processed", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn update_last_pruned<'a, E>(height: i64, executor: E) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("update_last_pruned");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("UPDATE pruning SET last_pruned_height = ?", height)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("update_last_pruned", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_blocks<'a, E>(oldest_to_keep: i64, executor: E) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("prune_blocks");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("DELETE FROM block WHERE height < ?", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_blocks", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_pre_commits<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("prune_pre_commits");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("DELETE FROM pre_commit WHERE height < ?", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_pre_commits", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_transactions<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("prune_transactions");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM \"transaction\" WHERE height < ?",
|
||||
oldest_to_keep
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_transactions", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_messages<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
trace!("prune_messages");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("DELETE FROM message WHERE height < ?", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_messages", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ use crate::block_processor::types::{FullBlockInformation, ParsedTransactionRespo
|
||||
use crate::error::ScraperError;
|
||||
use crate::storage::manager::{
|
||||
insert_block, insert_message, insert_precommit, insert_transaction, insert_validator,
|
||||
prune_blocks, prune_messages, prune_pre_commits, prune_transactions, update_last_processed,
|
||||
update_last_pruned, StorageManager,
|
||||
update_last_processed, StorageManager,
|
||||
};
|
||||
use crate::storage::models::{CommitSignature, Validator};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
@@ -16,7 +15,6 @@ use std::path::Path;
|
||||
use tendermint::block::{Commit, CommitSig};
|
||||
use tendermint::Block;
|
||||
use tendermint_rpc::endpoint::validators;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
mod helpers;
|
||||
@@ -30,19 +28,6 @@ pub struct ScraperStorage {
|
||||
pub(crate) manager: StorageManager,
|
||||
}
|
||||
|
||||
pub(crate) fn log_db_operation_time(op_name: &str, start_time: Instant) {
|
||||
let elapsed = start_time.elapsed();
|
||||
let formatted = humantime::format_duration(elapsed);
|
||||
|
||||
match elapsed.as_millis() {
|
||||
v if v > 10000 => error!("{op_name} took {formatted} to execute"),
|
||||
v if v > 1000 => warn!("{op_name} took {formatted} to execute"),
|
||||
v if v > 100 => info!("{op_name} took {formatted} to execute"),
|
||||
v if v > 10 => debug!("{op_name} took {formatted} to execute"),
|
||||
_ => trace!("{op_name} took {formatted} to execute"),
|
||||
}
|
||||
}
|
||||
|
||||
impl ScraperStorage {
|
||||
#[instrument]
|
||||
pub async fn init<P: AsRef<Path> + Debug>(database_path: P) -> Result<Self, ScraperError> {
|
||||
@@ -80,32 +65,6 @@ impl ScraperStorage {
|
||||
Ok(storage)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn prune_storage(
|
||||
&self,
|
||||
oldest_to_keep: u32,
|
||||
current_height: u32,
|
||||
) -> Result<(), ScraperError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut tx = self.begin_processing_tx().await?;
|
||||
|
||||
prune_messages(oldest_to_keep.into(), &mut tx).await?;
|
||||
prune_transactions(oldest_to_keep.into(), &mut tx).await?;
|
||||
prune_pre_commits(oldest_to_keep.into(), &mut tx).await?;
|
||||
prune_blocks(oldest_to_keep.into(), &mut tx).await?;
|
||||
update_last_pruned(current_height.into(), &mut tx).await?;
|
||||
|
||||
let commit_start = Instant::now();
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|source| ScraperError::StorageTxCommitFailure { source })?;
|
||||
log_db_operation_time("committing pruning tx", commit_start);
|
||||
|
||||
log_db_operation_time("pruning storage", start);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn begin_processing_tx(&self) -> Result<StorageTransaction, ScraperError> {
|
||||
debug!("starting storage tx");
|
||||
@@ -116,10 +75,6 @@ impl ScraperStorage {
|
||||
.map_err(|source| ScraperError::StorageTxBeginFailure { source })
|
||||
}
|
||||
|
||||
pub async fn lowest_block_height(&self) -> Result<Option<i64>, ScraperError> {
|
||||
Ok(self.manager.get_lowest_block().await?)
|
||||
}
|
||||
|
||||
pub async fn get_first_block_height_after(
|
||||
&self,
|
||||
time: OffsetDateTime,
|
||||
@@ -200,10 +155,6 @@ impl ScraperStorage {
|
||||
pub async fn get_last_processed_height(&self) -> Result<i64, ScraperError> {
|
||||
Ok(self.manager.get_last_processed_height().await?)
|
||||
}
|
||||
|
||||
pub async fn get_pruned_height(&self) -> Result<i64, ScraperError> {
|
||||
Ok(self.manager.get_pruned_height().await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn persist_block(
|
||||
|
||||
@@ -159,7 +159,7 @@ impl TunDevice {
|
||||
"add",
|
||||
&format!("{}/{}", ipv6, netmaskv6),
|
||||
"dev",
|
||||
(tun.name()),
|
||||
&tun.name(),
|
||||
])
|
||||
.output()?;
|
||||
Ok(tun)
|
||||
|
||||
@@ -50,7 +50,7 @@ pub struct DelegationWithEverything {
|
||||
pub accumulated_by_delegates: Option<DecCoin>,
|
||||
pub accumulated_by_operator: Option<DecCoin>,
|
||||
pub block_height: u64,
|
||||
pub delegated_on_iso_datetime: Option<String>,
|
||||
pub delegated_on_iso_datetime: String,
|
||||
pub cost_params: Option<MixNodeCostParams>,
|
||||
pub avg_uptime_percent: Option<u8>,
|
||||
|
||||
@@ -60,8 +60,6 @@ pub struct DelegationWithEverything {
|
||||
pub uses_vesting_contract_tokens: bool,
|
||||
pub unclaimed_rewards: Option<DecCoin>,
|
||||
|
||||
pub errors: Option<String>,
|
||||
|
||||
// DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT!
|
||||
pub pending_events: Vec<DelegationEvent>,
|
||||
pub mixnode_is_unbonding: Option<bool>,
|
||||
|
||||
@@ -171,25 +171,3 @@ impl fmt::Display for GatewayIpPacketRouterDetails {
|
||||
writeln!(f, "\taddress: {}", self.address)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GatewayWireguardDetails {
|
||||
pub enabled: bool,
|
||||
|
||||
pub announced_port: u16,
|
||||
pub private_network_prefix: u8,
|
||||
}
|
||||
|
||||
impl fmt::Display for GatewayWireguardDetails {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "wireguard:")?;
|
||||
writeln!(f, "\tenabled: {}", self.enabled)?;
|
||||
|
||||
writeln!(f, "\tannounced_port: {}", self.announced_port)?;
|
||||
writeln!(
|
||||
f,
|
||||
"\tprivate_network_prefix: {}",
|
||||
self.private_network_prefix
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
nym-config = { path = "../config" }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
|
||||
nym-network-defaults = { path = "../network-defaults" }
|
||||
|
||||
# feature-specific dependencies:
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
|
||||
pub struct Config {
|
||||
/// Socket address this node will use for binding its wireguard interface.
|
||||
/// default: `0.0.0.0:51822`
|
||||
pub bind_address: SocketAddr,
|
||||
|
||||
/// Private IP address of the wireguard gateway.
|
||||
/// default: `10.1.0.1`
|
||||
pub private_ip: IpAddr,
|
||||
|
||||
/// Port announced to external clients wishing to connect to the wireguard interface.
|
||||
/// Useful in the instances where the node is behind a proxy.
|
||||
pub announced_port: u16,
|
||||
|
||||
/// The prefix denoting the maximum number of the clients that can be connected via Wireguard.
|
||||
/// The maximum value for IPv4 is 32 and for IPv6 is 128
|
||||
pub private_network_prefix: u8,
|
||||
}
|
||||
@@ -1,51 +1,15 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use dashmap::DashMap;
|
||||
use nym_crypto::asymmetric::encryption::KeyPair;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod public_key;
|
||||
pub mod registration;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use public_key::PeerPublicKey;
|
||||
pub use registration::{
|
||||
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, GatewayClientRegistry,
|
||||
InitMessage, Nonce,
|
||||
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce,
|
||||
};
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
pub use registration::HmacSha256;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WireguardGatewayData {
|
||||
config: Config,
|
||||
keypair: Arc<KeyPair>,
|
||||
client_registry: Arc<GatewayClientRegistry>,
|
||||
}
|
||||
|
||||
impl WireguardGatewayData {
|
||||
pub fn new(config: Config, keypair: Arc<KeyPair>) -> Self {
|
||||
WireguardGatewayData {
|
||||
config,
|
||||
keypair,
|
||||
client_registry: Arc::new(DashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Config {
|
||||
self.config
|
||||
}
|
||||
|
||||
pub fn keypair(&self) -> &Arc<KeyPair> {
|
||||
&self.keypair
|
||||
}
|
||||
|
||||
pub fn client_registry(&self) -> &Arc<GatewayClientRegistry> {
|
||||
&self.client_registry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,6 @@ impl PeerPublicKey {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> PublicKey {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PeerPublicKey {
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{fmt, ops::Deref, str::FromStr};
|
||||
#[cfg(feature = "verify")]
|
||||
use hmac::{Hmac, Mac};
|
||||
#[cfg(feature = "verify")]
|
||||
use nym_crypto::asymmetric::encryption::PrivateKey;
|
||||
use nym_crypto::asymmetric::encryption::{PrivateKey, PublicKey};
|
||||
#[cfg(feature = "verify")]
|
||||
use sha2::Sha256;
|
||||
|
||||
@@ -87,7 +87,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn new(
|
||||
local_secret: &PrivateKey,
|
||||
remote_public: x25519_dalek::PublicKey,
|
||||
remote_public: PublicKey,
|
||||
private_ip: IpAddr,
|
||||
nonce: u64,
|
||||
) -> Self {
|
||||
@@ -96,6 +96,8 @@ impl GatewayClient {
|
||||
let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes());
|
||||
let local_public: x25519_dalek::PublicKey = (&static_secret).into();
|
||||
|
||||
let remote_public = x25519_dalek::PublicKey::from(remote_public.to_bytes());
|
||||
|
||||
let dh = static_secret.diffie_hellman(&remote_public);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
|
||||
+18
-15
@@ -3,37 +3,40 @@
|
||||
// #![warn(clippy::expect_used)]
|
||||
// #![warn(clippy::unwrap_used)]
|
||||
|
||||
pub mod setup;
|
||||
|
||||
/// Start wireguard device
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn start_wireguard(
|
||||
mut task_client: nym_task::TaskClient,
|
||||
wireguard_data: std::sync::Arc<nym_wireguard_types::WireguardGatewayData>,
|
||||
_gateway_client_registry: std::sync::Arc<
|
||||
nym_wireguard_types::registration::GatewayClientRegistry,
|
||||
>,
|
||||
) -> Result<defguard_wireguard_rs::WGApi, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
use crate::setup::{peer_allowed_ips, peer_static_public_key, PRIVATE_KEY};
|
||||
use defguard_wireguard_rs::{
|
||||
host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi,
|
||||
};
|
||||
|
||||
let mut peers = vec![];
|
||||
for peer_client in wireguard_data.client_registry().iter() {
|
||||
let mut peer = Peer::new(Key::new(peer_client.pub_key.to_bytes()));
|
||||
let peer_ip_mask = IpAddrMask::new(peer_client.private_ip, 32);
|
||||
peer.set_allowed_ips(vec![peer_ip_mask]);
|
||||
peers.push(peer);
|
||||
}
|
||||
use nym_network_defaults::{WG_PORT, WG_TUN_DEVICE_ADDRESS};
|
||||
|
||||
let ifname = String::from("wg0");
|
||||
let wgapi = WGApi::new(ifname.clone(), false)?;
|
||||
wgapi.create_interface()?;
|
||||
let interface_config = InterfaceConfiguration {
|
||||
name: ifname.clone(),
|
||||
prvkey: BASE64_STANDARD.encode(wireguard_data.keypair().private_key().to_bytes()),
|
||||
address: wireguard_data.config().private_ip.to_string(),
|
||||
port: wireguard_data.config().announced_port as u32,
|
||||
peers,
|
||||
prvkey: PRIVATE_KEY.to_string(),
|
||||
address: WG_TUN_DEVICE_ADDRESS.to_string(),
|
||||
port: WG_PORT as u32,
|
||||
peers: vec![],
|
||||
};
|
||||
wgapi.configure_interface(&interface_config)?;
|
||||
// wgapi.configure_peer_routing(&peers)?;
|
||||
let peer = peer_static_public_key();
|
||||
let mut peer = Peer::new(Key::new(peer.to_bytes()));
|
||||
let peer_ip = peer_allowed_ips();
|
||||
let peer_ip_mask = IpAddrMask::new(peer_ip.network_address(), peer_ip.netmask());
|
||||
peer.set_allowed_ips(vec![peer_ip_mask]);
|
||||
wgapi.configure_peer(&peer)?;
|
||||
wgapi.configure_peer_routing(&[peer.clone()])?;
|
||||
|
||||
tokio::spawn(async move { task_client.recv().await });
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use log::info;
|
||||
|
||||
// The wireguard UDP listener
|
||||
pub const WG_ADDRESS: &str = "0.0.0.0";
|
||||
|
||||
// The private key of the listener
|
||||
// Corresponding public key: "WM8s8bYegwMa0TJ+xIwhk+dImk2IpDUKslDBCZPizlE="
|
||||
pub(crate) const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg=";
|
||||
|
||||
// The AllowedIPs for the connected peer, which is one a single IP and the same as the IP that the
|
||||
// peer has configured on their side.
|
||||
const ALLOWED_IPS: &str = "10.1.0.2";
|
||||
|
||||
fn decode_base64_key(base64_key: &str) -> [u8; 32] {
|
||||
general_purpose::STANDARD
|
||||
.decode(base64_key)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn server_static_private_key() -> x25519_dalek::StaticSecret {
|
||||
// TODO: this is a temporary solution for development
|
||||
let static_private_bytes: [u8; 32] = decode_base64_key(PRIVATE_KEY);
|
||||
let static_private = x25519_dalek::StaticSecret::from(static_private_bytes);
|
||||
let static_public = x25519_dalek::PublicKey::from(&static_private);
|
||||
info!(
|
||||
"wg public key: {}",
|
||||
general_purpose::STANDARD.encode(static_public)
|
||||
);
|
||||
static_private
|
||||
}
|
||||
|
||||
pub fn peer_static_public_key() -> x25519_dalek::PublicKey {
|
||||
// A single static public key is used during development
|
||||
|
||||
// Read from NYM_PEER_PUBLIC_KEY env variable
|
||||
let peer = std::env::var("NYM_PEER_PUBLIC_KEY").expect("NYM_PEER_PUBLIC_KEY must be set");
|
||||
|
||||
let peer_static_public_bytes: [u8; 32] = decode_base64_key(&peer);
|
||||
let peer_static_public = x25519_dalek::PublicKey::from(peer_static_public_bytes);
|
||||
info!(
|
||||
"Adding wg peer public key: {}",
|
||||
general_purpose::STANDARD.encode(peer_static_public)
|
||||
);
|
||||
peer_static_public
|
||||
}
|
||||
|
||||
pub fn peer_allowed_ips() -> ip_network::IpNetwork {
|
||||
let key: IpAddr = ALLOWED_IPS.parse().unwrap();
|
||||
let cidr = 32u8;
|
||||
ip_network::IpNetwork::new_truncate(key, cidr).unwrap()
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](introduction.md)
|
||||
- [Changelog](changelog.md)
|
||||
|
||||
# Binaries
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
This page displays a full list of all the changes during our release cycle from [`v2024.3-eclipse`](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md) onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first.
|
||||
|
||||
## `v2024.4-nutella`
|
||||
|
||||
- [Merged PRs](https://github.com/nymtech/nym/milestone/59?closed=1)
|
||||
- [`nym-node`](nodes/nym-node.md) version `1.1.1`
|
||||
- This release also contains: `nym-gateway` and `nym-network-requester` binaries
|
||||
- core improvements on nym-node configuration
|
||||
- Nym wallet changes:
|
||||
- Adding `nym-node` command to bonding screens
|
||||
- Fixed the delegation issues with fixing RPC
|
||||
- [Network configuration](nodes/configuration.md#connectivity-test-and-configuration) section updates, in particular for `--mode mixnode` operators
|
||||
- [VPS IPv6 troubleshooting](troubleshooting/vps-isp.md#ipv6-troubleshooting) updates
|
||||
|
||||
## `v2024.3-eclipse`
|
||||
|
||||
- Release [Changelog.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md)
|
||||
- [`nym-node`](nodes/nym-node.md) initial release
|
||||
- New tool for monitoring Gateways performance [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net)
|
||||
- New versioning `1.1.0+nymnode` mainly for internal migration testing, not essential for operational use. We aim to correct this in a future release to ensure mixnodes feature correctly in the main API
|
||||
- New [VPS specs & configuration](nodes/vps-setup.md) page
|
||||
- New [configuration page](nodes/configuration.md) with [connectivity setup guide](nodes/configuration.md#connectivity-test-and-configuration) - a new requirement for `exit-gateway`
|
||||
- API endpoints redirection: Nym-mixnode and nym-gateway endpoints will eventually be deprecated; due to this, their endpoints will be redirected to new routes once the `nym-node` has been migrated and is running
|
||||
|
||||
**API endpoints redirection**
|
||||
|
||||
| Previous endpoint | New endpoint |
|
||||
| --- | --- |
|
||||
| `http://<IP>:8000/stats` | `http://<IP>:8000/api/v1/metrics/mixing` |
|
||||
| `http://<IP>:8000/hardware` | `http://<IP>:8000/api/v1/system-info` |
|
||||
| `http://<IP>:8000/description` | `http://<IP>:8000/api/v1/description` |
|
||||
@@ -161,49 +161,15 @@ This lets your operating system know it's ok to reload the service configuration
|
||||
|
||||
## Connectivity Test and Configuration
|
||||
|
||||
```admonish info
|
||||
**This chapter is relevant only for operators running an `exit-gateway` mode.** If this is not your case, please proceed to [bonding](bonding.md).
|
||||
```
|
||||
|
||||
During our ongoing testing events [Fast and Furious](https://nymtech.net/events/fast-and-furious) we found out, that after introducing IP Packet Router (IPR) and [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) on embedded Network Requester (NR) by default, only a fragment of Gateways routes correctly through IPv4 and IPv6. We built a useful monitor to check out your Gateway (`nym-node --mode exit-gateway`) at [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net/).
|
||||
|
||||
IPv6 routing is not only a case for gateways. Imagine a rare occassion when you run a `mixnode` without IPv6 enabled and a client will sent IPv6 packets through the Mixnet through such route:
|
||||
```ascii
|
||||
[client] -> [entry-gateway] -> [mixnode layer 1] -> [your mixnode] -> [IPv6 mixnode layer3] -> [exit-gateway]
|
||||
```
|
||||
In this (unusual) case your `mixnode` will not be able to route the packets. The node will drop the packets and its performance would go down. For that reason it's befetial to have IPv6 enabled when running a `mixnode` functionality.
|
||||
|
||||
### Quick IPv6 Check
|
||||
|
||||
```admonish caution
|
||||
Make sure to keep your IPv4 address enabled while setting up IPv6, as the majority of routing goes through that one!
|
||||
```
|
||||
|
||||
You can always check IPv6 address and connectivity by using some of these methods:
|
||||
|
||||
```sh
|
||||
# locally listed IPv6 addresses
|
||||
ip -6 addr
|
||||
|
||||
# globally reachable IPv6 addresses
|
||||
ip -6 addr show scope global
|
||||
|
||||
# with DNS
|
||||
dig -6 TXT +short o-o.myaddr.l.google.com @ns1.google.com
|
||||
dig -t aaaa +short myip.opendns.com @resolver1.opendns.com
|
||||
|
||||
# https check
|
||||
curl -6 https://ifconfig.co
|
||||
curl -6 https://ipv6.icanhazip.com
|
||||
|
||||
# using telnet
|
||||
telnet -6 ipv6.telnetmyip.com
|
||||
```
|
||||
|
||||
### IPv6 Configuration
|
||||
|
||||
While we're working on Rust implementation to have these settings as a part of the binary build, we wrote a script to solve these connectivity requirements in the meantime we wrote a script [`network_tunnel_manager.sh`](https://gist.github.com/tommyv1987/ccf6ca00ffb3d7e13192edda61bb2a77) to support the operators to configure their servers and address all the connectivity requirements.
|
||||
|
||||
Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](../troubleshooting/vps-isp.md#ipv6-troubleshooting) page. Be aware that you may have to do more research and customised adjustments.
|
||||
|
||||
#### Mode: `exit-gateway`
|
||||
|
||||
The `nymtun0` interface is dynamically managed by the `exit-gateway` service. When the service is stopped, `nymtun0` disappears, and when started, `nymtun0` is recreated.
|
||||
|
||||
The script should be used in a context where `nym-node --mode exit-gateway` is running to fully utilise its capabilities, particularly for fetching IPv6 addresses or applying network rules that depend on the `nymtun0` interface.
|
||||
@@ -237,7 +203,7 @@ alongside diagnostics for verifying system settings and network connectivity.
|
||||
```
|
||||
~~~
|
||||
|
||||
- To run the script next time, just enter `./network_tunnel_manager <ARG>`
|
||||
- To run the script next time, just enter `./network_tunnel_manager.`
|
||||
|
||||
2. Make sure your `nym-node --mode exit-gateway` service is up running
|
||||
|
||||
@@ -299,11 +265,11 @@ operation fetch_ipv6_address_nym_tun completed successfully.
|
||||
sudo ./network_tunnel_manager.sh apply_iptables_rules
|
||||
```
|
||||
|
||||
- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes.
|
||||
- The process may prompt you if you want to save current IPv4 rules, choose yes.
|
||||
|
||||

|
||||
|
||||
- check IPv6 again like in point 3
|
||||
- and check them again like in point 3
|
||||
|
||||
6. At this point your node needs to be [bonded](bonding.md) to the API for `nymtun0` to interact with the network. After bonding please follow up with the remaining streps below to ensure that your Exit Gateway is routing properly.
|
||||
|
||||
@@ -332,75 +298,6 @@ sudo ./network_tunnel_manager.sh joke_through_the_mixnet
|
||||
|
||||
Make sure that you get the validation of IPv4 and IPv6 connectivity. If there are still any problems, please refer to [troubleshooting section](../troubleshooting/vps-isp.md#incorrect-gateway-network-check).
|
||||
|
||||
#### Mode: `mixnode`
|
||||
|
||||
```admonish caution title=""
|
||||
Most of the time the packets sent through the Mixnet are IPv4 based. The IPv6 packets are still pretty rare and therefore it's not mandatory from operational point of view. If you preparing to run a `nym-node` with all modes enabled once this option is implemented, then the IPv6 setup on your VPS is required.
|
||||
```
|
||||
|
||||
1. Download `network_tunnel_manager.sh`, make executable and run:
|
||||
|
||||
```sh
|
||||
curl -o network_tunnel_manager.sh -L https://gist.githubusercontent.com/tommyv1987/ccf6ca00ffb3d7e13192edda61bb2a77/raw/9d785d6ee3aa2970553633eccbd89a827f49fab5/network_tunnel_manager.sh && chmod +x network_tunnel_manager.sh && ./network_tunnel_manager.sh
|
||||
```
|
||||
|
||||
Here is a quick command explanation, for more details on the `network_tunnel_manager.sh` script, refer to the [overview](https://gist.github.com/tommyv1987/ccf6ca00ffb3d7e13192edda61bb2a77) under the code block. Mind that for `mixnode` VPS setup we will use only a few of the commands.
|
||||
|
||||
~~~admonish example collapsible=true title="A summarized usage of `network_tunnel_manager.sh`"
|
||||
```sh
|
||||
summary:
|
||||
This is a comprehensive script for configuring network packet forwarding and iptables rules,
|
||||
aimed at ensuring smooth operation of a tunnel interface.
|
||||
It includes functionality for both setup and tear-down of nymtun network configurations,
|
||||
alongside diagnostics for verifying system settings and network connectivity.
|
||||
|
||||
* fetch_ipv6_address_nym_tun - Fetches the IPv6 address assigned to the 'nymtun0'.
|
||||
* fetch_and_display_ipv6 - Displays the IPv6 address on the default network device.
|
||||
* apply_iptables_rules - Applies necessary IPv4 and IPv6 iptables rules.
|
||||
* remove_iptables_rules - Removes applied IPv4 and IPv6 iptables rules.
|
||||
* check_ipv6_ipv4_forwarding - Checks if IPv4 and IPv6 forwarding are enabled.
|
||||
* check_nymtun_iptables - Check nymtun0 device
|
||||
* perform_ipv4_ipv6_pings - Perform ipv4 and ipv6 pings to google
|
||||
* check_ip6_ipv4_routing - Check ipv6 and ipv4 routing
|
||||
* joke_through_the_mixnet - Run a joke through the mixnet via ipv4 and ipv6
|
||||
|
||||
```
|
||||
~~~
|
||||
|
||||
- To run the script next time, just enter `./network_tunnel_manager <ARG>`
|
||||
|
||||
2. Display IPv6:
|
||||
```sh
|
||||
sudo ./network_tunnel_manager.sh fetch_and_display_ipv6
|
||||
```
|
||||
- if you have a `global ipv6` address this is good
|
||||
|
||||
~~~admonish example collapsible=true title="Correct `./network_tunnel_manager.sh fetch_and_display_ipv6` output:"
|
||||
```sh
|
||||
iptables-persistent is already installed.
|
||||
Using IPv6 address: 2001:db8:a160::1/112 #the address will be different for you
|
||||
operation fetch_ipv6_address_nym_tun completed successfully.
|
||||
```
|
||||
~~~
|
||||
|
||||
3. Apply the rules:
|
||||
```sh
|
||||
sudo ./network_tunnel_manager.sh apply_iptables_rules
|
||||
```
|
||||
|
||||
- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes.
|
||||
|
||||

|
||||
|
||||
- check IPv6 again like in point 2
|
||||
|
||||
4. Check connectivity
|
||||
```sh
|
||||
telnet -6 ipv6.telnetmyip.com
|
||||
```
|
||||
|
||||
Make sure that you get the validation of IPv4 and IPv6 connectivity. If there are still any problems, please refer to [troubleshooting section](../troubleshooting/vps-isp.md#incorrect-gateway-network-check).
|
||||
|
||||
## Next Steps
|
||||
|
||||
There are a few more good suggestions for `nym-node` VPS configuration, especially to be considered for `exit-gateway` functionality, like Web Secure Socket or Reversed Proxy setup. Visit [Proxy configuration](proxy-configuration.md) page to see the guides.
|
||||
|
||||
@@ -205,79 +205,33 @@ This lets your operating system know it's ok to reload the service configuration
|
||||
|
||||
## Moving a node
|
||||
|
||||
In case of a need to move a Nym Node from one machine to another and avoiding to lose the delegation, here are few steps how to do it.
|
||||
In case of a need to move a node from one machine to another and avoiding to lose the delegation, here are few steps how to do it.
|
||||
|
||||
The following examples transfers a Mix Node (in case of other nodes, change the `mixnodes` in the command for the `<NODE>` of your desire.
|
||||
|
||||
* Pause your node process.
|
||||
|
||||
Assuming both machines are remote VPS.
|
||||
|
||||
* Make sure your `~/.ssh/<YOUR_KEY>.pub` is in both of the servers `~/.ssh/authorized_keys` file
|
||||
* Make sure your `~/.ssh/<YOUR_KEY>.pub` is in both of the machines `~/.ssh/authorized_keys` file
|
||||
* Create a `nym-nodes` folder in the target VPS. SSH in from your terminal and run:
|
||||
|
||||
```sh
|
||||
# in case none of the nym configs was created previously
|
||||
mkdir ~/.nym
|
||||
|
||||
#in case no nym Nym Node was initialized previously
|
||||
#in case no nym Mix Node was initialized previously
|
||||
mkdir ~/.nym/nym-nodes
|
||||
```
|
||||
* Move the node data (keys) and config file to the new machine by opening your **local terminal** (as that one's ssh key is authorized in both of the VPS) and running:
|
||||
* Move the node data (keys) and config file to the new machine by opening your **local terminal** (as that one's ssh key is authorized in both of the machines) and running:
|
||||
```sh
|
||||
scp -r -3 <SOURCE_USER_NAME>@<SOURCE_HOST_ADDRESS>:~/.nym/nym-nodes <TARGET_USER_NAME>@<TARGET_HOST_ADDRESS>:~/.nym/nym-nodes/
|
||||
```
|
||||
|
||||
**On new/target machine**
|
||||
|
||||
* Edit `~/.nym/nym-nodes/<ID>/config/config.toml` config with the new listening address IP.
|
||||
* Setup the [systemd](#systemd) automation, reload the daemon and run the service, or just simply run the node if you don't use automation
|
||||
* Re-initialise (`run` command) the node to generate a config with the new listening address.
|
||||
* Change the node smart contract info via the wallet interface. Otherwise the keys will point to the old IP address in the smart contract, and the node will not be able to be connected, and it will fail up-time checks.
|
||||
* Re-run the node from the new location.
|
||||
|
||||
## Rename node local ID
|
||||
|
||||
Local node ID (not the identity key) is a name chosen by operators which defines where the nodes configuration data will be stored, where the ID determines the path to `~/.nym/nym-nodes/<ID>/`. This ID is never shared on the network.
|
||||
|
||||
Since migrating to [`nym-node`](nym-node.md), specifying an with `--ID <ID>` when starting a new node is no longer necessary. Nodes without a specified ID will be asigned the default ID `default-nym-node`. This streamlines node management, particularly for operators handling multiple nodes via ansible and other automation scripts, as all data is stored at `~/.nym/nym-nodes/default-nym-node`.
|
||||
|
||||
If you already operate a `nym-node` and wish to change the local ID to `default-nym-node` or anything else, follow the steps below to do so.
|
||||
|
||||
```admonish note
|
||||
In the example we use `default-nym-node` as a target `<ID>`, if you prefer to use another name, edit the syntax in the commands accordingly.
|
||||
```
|
||||
|
||||
1. Copy the configuration directory to the new one
|
||||
```sh
|
||||
cp -r ~/.nym/nym-nodes/<SOURCE_ID> ~/.nym/nym-nodes/default-nym-node/
|
||||
```
|
||||
|
||||
2. Rename all `<SOURCE_ID>` occurences in `config.toml` to `default-nym-node`
|
||||
|
||||
```sh
|
||||
# check occurences of the <SOURCE_ID>
|
||||
grep -r "<SOURCE_ID>" ~/.nym/nym-nodes/default-nym-node/*
|
||||
```
|
||||
```admonish bug title="Caution!"
|
||||
If your node `<SOURCE_ID>` is too generic (like `gateway` etc) and it occurs elsewhere than just a custom value, **do not use `sed` command but rewrite the values manually using a text editor!**
|
||||
```
|
||||
|
||||
```sh
|
||||
# rename it by using sed command
|
||||
sed -i -e "s/<SOURCE_ID>/default-nym-node/g" ~/.nym/nym-nodes/default-nym-node/config/config.toml
|
||||
|
||||
# or manually by opening config.toml and rewriting each occurence of <SOURCE_ID>
|
||||
nano ~/.nym/nym-nodes/default-nym-node/config/config.toml
|
||||
```
|
||||
|
||||
3. Validate by rechecking the config file content
|
||||
```sh
|
||||
# either re-run
|
||||
grep -r "<SOURCE_ID>" ~/.nym/nym-nodes/default-nym-node/*
|
||||
|
||||
# or by reading the config file
|
||||
less ~/.nym/nym-nodes/default-nym-node/config/config.toml
|
||||
```
|
||||
- Pay extra attention to the `hostname` line. In case its value was somehow correlated with the `<SOURCE_ID>` string you may need to correct it back
|
||||
|
||||
4. Reload your [systemd service daemon](#systemd) and restart the service, or if automation isn't your thing, simply reboot the node
|
||||
|
||||
5. If you double-checked that everything works fine, you can consider removing your old config directory
|
||||
|
||||
## Ports
|
||||
All `<NODE>`-specific port configuration can be found in `$HOME/.nym/<NODE>/<YOUR_ID>/config/config.toml`. If you do edit any port configs, remember to restart your client and node processes.
|
||||
|
||||
@@ -13,7 +13,7 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](n
|
||||
```
|
||||
|
||||
```admonish note
|
||||
If you are a `nym-mixnode` or `nym-gateway` operator and you are not familiar with the binary changes called *Project Smoosh*, you can read the archived [Smoosh FAQ](../archive/faq/smoosh-faq.md) page.
|
||||
If you are a `nym-mixnode` or `nym-gateway` operator and you are not familiar wwith the binary changes called *Project Smoosh*, you can read the archived [Smoosh FAQ](../archive/smoosh-faq.md) page.
|
||||
```
|
||||
|
||||
## Summary
|
||||
@@ -148,8 +148,8 @@ Options:
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `0.0.0.0:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-private-gw-ip <WIREGUARD_PRIVATE_IP>
|
||||
Private IP address of the wireguard gateway. default: `10.1.0.1` [env: NYMNODE_WG_IP=]
|
||||
--wireguard-private-network-ip <WIREGUARD_PRIVATE_NETWORK_IP>
|
||||
Ip address of the private wireguard network. default: `10.1.0.0` [env: NYMNODE_WG_IP_NETWORK=]
|
||||
--wireguard-announced-port <WIREGUARD_ANNOUNCED_PORT>
|
||||
Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
@@ -278,11 +278,9 @@ Run the node with custom `--id` without initialising:
|
||||
### Migrate
|
||||
|
||||
```admonish caution
|
||||
Migration is a must for all deprecated nodes (`nym-mixnode`, `nym-gateway`). For backward compatibility we created an [archive section](../archive/nodes/setup-guides.md) with all the guides for individual binaries. However, the binaries from version 1.1.35 (`nym-gateway`) and 1.1.37 (`nym-mixnode`) onwards will no longer have `init` command.
|
||||
Migration is a must for all deprecated nodes (`nym-mixnode`, `nym-gateway`). For backward compatibility we created an [archive section](../archive/setup-guides.md) with all the guides for individual binaries. However, the binaries from version 1.1.35 (`nym-gateway`) and 1.1.37 (`nym-mixnode`) onwards will no longer have `init` command.
|
||||
```
|
||||
|
||||
Operators who are about to migrate their nodes need to configure their [VPS](vps-setup.md) and setup `nym-node` which can be downloaded as a [pre-built binary](../binaries/pre-built-binaries.md) or compiled from [source](../binaries/building-nym.md).
|
||||
|
||||
To migrate a `nym-mixnode` or a `nym-gateway` to `nym-node` is fairly simple, use the `migrate` command with `--config-file` flag pointing to the original `config.toml` file, with a conditional argument defining which type of node this configuration belongs to. Examples are below.
|
||||
|
||||
Make sure to use `--deny-init` flag to prevent initialisation of a new node.
|
||||
|
||||
@@ -58,22 +58,6 @@ To install a full node from scratch, refer to the [validator setup guide](valida
|
||||
|
||||
Before node or validator setup, the VPS needs to be configured and tested, to verify your connectivity and make sure that your provider wasn't dishonest with the offered services.
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
SSH to your server as `root` or become one running `sudo -i` or `su`. If you prefer to administrate your VPS from a user environment, supply the commands with prefix `sudo`.
|
||||
|
||||
Start with setting up the essential tools on your server.
|
||||
```sh
|
||||
# get your system up to date
|
||||
apt update -y && apt --fix-broken install
|
||||
|
||||
# install dependencies
|
||||
apt -y install ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git
|
||||
|
||||
# double check ufw is installed correctly
|
||||
apt install ufw --fix-missing
|
||||
```
|
||||
|
||||
### Configure your Firewall
|
||||
|
||||
For a `nym-node` or Nyx validator to recieve traffic, you need to open ports on the server. The following commands will allow you to set up a firewall using `ufw`.
|
||||
@@ -84,33 +68,33 @@ For a `nym-node` or Nyx validator to recieve traffic, you need to open ports on
|
||||
ufw version
|
||||
|
||||
# if it is not installed, install with
|
||||
apt install ufw -y
|
||||
sudo apt install ufw -y
|
||||
|
||||
# enable ufw
|
||||
ufw enable
|
||||
sudo ufw enable
|
||||
|
||||
# check the status of the firewall
|
||||
ufw status
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
2. Open all needed ports to have your firewall working correctly:
|
||||
```sh
|
||||
# for nym-node
|
||||
ufw allow 1789,1790,8000,9000,9001,22/tcp
|
||||
sudo ufw allow 1789,1790,8000,9000,9001,22/tcp
|
||||
|
||||
# in case of planning to setup a WSS (for Gateway functionality)
|
||||
ufw allow 9001/tcp
|
||||
sudo ufw allow 9001/tcp
|
||||
|
||||
# in case of reverse proxy for the swagger page (for Gateway optionality)
|
||||
ufw allow 8080,80,443
|
||||
sudo ufw allow 8080,80,443
|
||||
|
||||
# for validator
|
||||
ufw allow 1317,26656,26660,22,80,443/tcp
|
||||
sudo ufw allow 1317,26656,26660,22,80,443/tcp
|
||||
```
|
||||
|
||||
3. Check the status of the firewall:
|
||||
```sh
|
||||
ufw status
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
For more information about your node's port configuration, check the [port reference table](#ports-reference-table) below.
|
||||
|
||||
@@ -191,38 +191,14 @@ If you are still unable to see your node on the dashboard, or your node is decla
|
||||
- The firewall on your host machine is not configured properly. Checkout the [instructions](../nodes/vps-setup.md#configure-your-firewall).
|
||||
- You provided incorrect information when bonding your node.
|
||||
- You are running your node from a VPS without IPv6 support.
|
||||
<!-- You did not use the `--announce-host` flag while running the Mix Node from your local machine behind NAT. -->
|
||||
- You did not configure your router firewall while running the node from your local machine behind NAT, or you are lacking IPv6 support
|
||||
- Your Mix Node is not running at all, it has either exited / panicked or you closed the session without making the node persistent. Check out the [instructions](../nodes/configuration.md#automating-your-node-with-tmux-and-systemd).
|
||||
|
||||
```admonish caution title=""
|
||||
```admonish caution
|
||||
Your Nym Node **must speak both IPv4 and IPv6** in order to cooperate with other nodes and route traffic. This is a common reason behind many errors we are seeing among node operators, so check with your provider that your VPS is able to do this!
|
||||
```
|
||||
|
||||
#### Check IPv6 Connectivity
|
||||
|
||||
You can always check IPv6 address and connectivity by using some of these methods:
|
||||
|
||||
```sh
|
||||
# locally listed IPv6 addresses
|
||||
ip -6 addr
|
||||
|
||||
# globally reachable IPv6 addresses
|
||||
ip -6 addr show scope global
|
||||
|
||||
# with DNS
|
||||
dig -6 TXT +short o-o.myaddr.l.google.com @ns1.google.com
|
||||
dig -t aaaa +short myip.opendns.com @resolver1.opendns.com
|
||||
|
||||
# https check
|
||||
curl -6 https://ifconfig.co
|
||||
curl -6 https://ipv6.icanhazip.com
|
||||
|
||||
# using telnet
|
||||
telnet -6 ipv6.telnetmyip.com
|
||||
```
|
||||
If your connection doesn't work make sure to follow [VPS IPv6 setup](../nodes/configuration.md#connectivity-test-and-configuration). If there is more troubleshooting needed, check out [VPS IPv6 troubleshooting](vps-isp.md#ipv6-troubleshooting) page.
|
||||
|
||||
|
||||
#### Incorrect bonding information
|
||||
|
||||
Check that you have provided the correct information when bonding your Nym Node in the web wallet interface. When in doubt, un-bond and then re-bond your node!
|
||||
@@ -235,6 +211,33 @@ On certain cloud providers such as AWS and Google Cloud, you need to do some add
|
||||
|
||||
If the difference between the two is unclear, contact the help desk of your VPS provider.
|
||||
|
||||
#### No IPv6 connectivity
|
||||
|
||||
Make sure that your VPS has IPv6 connectivity available with whatever provider you are using.
|
||||
|
||||
To get all ip addresses of your host, try following commands:
|
||||
|
||||
```
|
||||
hostname -i
|
||||
```
|
||||
|
||||
Will return your **local ip** address.
|
||||
|
||||
```
|
||||
hostname -I
|
||||
```
|
||||
|
||||
Will return all of the ip addresses of your host. This output should look something like this:
|
||||
|
||||
```
|
||||
bob@nym:~$ hostname -I
|
||||
88.36.11.23 172.18.0.1 2a01:28:ca:102::1:641
|
||||
```
|
||||
|
||||
- The first **ipv4** is the public ip you need to use for the `--announce-host` flag.
|
||||
- The second **ipv4** is the local ip you need to use for the `--host` flag.
|
||||
- The 3rd output should confirm if your machine has ipv6 available.
|
||||
|
||||
### Running on a local machine behind NAT with no fixed IP address
|
||||
|
||||
Your ISP has to be IPv6 ready if you want to run a Nym Node on your local machine. Sadly, in 2020, most of them are not and you won't get an IPv6 address by default from your ISP. Usually it is an extra paid service or they simply don't offer it.
|
||||
@@ -271,7 +274,7 @@ thread 'tokio-runtime-worker' panicked at 'Failed to create TCP listener: Os { c
|
||||
```
|
||||
|
||||
Then you need to `--announce-host <PUBLIC_IP>` and `--host <LOCAL_IP>` on startup. This issue is addressed [above](#missing-`announce-host`-flag)
|
||||
<!-- NEEDS TO BE REWORKED AND ADD WARNING TO NOT CHANGE OTHER PORTS FOR API
|
||||
|
||||
### Can I use a port other than 1789?
|
||||
|
||||
Yes! Here is what you will need to do:
|
||||
@@ -298,7 +301,7 @@ nano ~/.nym/nym-nodes/alice-node/config/config.toml
|
||||
You will need to edit two parts of the file. `announce_address` and `listening_address` in the `config.toml` file. Simply replace `:1789` (the default port) with `:1337` (your new port) after your IP address.
|
||||
|
||||
Finally, restart your node. You should see if the Mix Node is using the port you have changed in the config.toml file right after you run the node.
|
||||
-->
|
||||
|
||||
### What is `verloc` and do I have to configure my Nym Node to implement it?
|
||||
|
||||
`verloc` is short for _verifiable location_. Mix Nodes and Gateways now measure speed-of-light distances to each other, in an attempt to verify how far apart they are. In later releases, this will allow us to algorithmically verify node locations in a non-fake-able and trustworthy manner.
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
# Troubleshooting VPS Setup
|
||||
|
||||
```admonish info
|
||||
To monitor the connectivity of your Exit Gateway, use results of probe testing displayed in [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net).
|
||||
```
|
||||
## Incorrect Gateway Network Check
|
||||
|
||||
## IPv6 troubleshooting
|
||||
|
||||
### Incorrect Gateway Network Check
|
||||
|
||||
Nym operators community is working on a Nym version of tors [good bad ISP table](https://community.torproject.org/relay/community-resources/good-bad-isps/). There is no one solution fits all when it comes to connectivity setup. The operation of `nym-node` will vary depending on your ISP and chosen system/distribution. While few machines will work out of the box, most will work after uisng our connectivity configuration guide, some need more adjustments.
|
||||
|
||||
Begin with the steps listed in [*Connectivity Test and Configuration*](../nodes/vps-setup.md#connectivity-test-and-configuration) chapter of VPS Setup page. If you still have a problem with the IPv6 connectivity try:
|
||||
If you followed all the steps listed in [Connectivity Test and Configuration](../nodes/vps-setup.md#connectivity-test-and-configuration) chapter of VPS Setup and you still have a problem with a correct connectivity for page in
|
||||
|
||||
1. Tor community created a helpful [table of ISPs](https://community.torproject.org/relay/community-resources/good-bad-isps/). Make sure your one is listed there as a *"good ISP"*. If not, consider migrating!
|
||||
2. Checkout your VPS dashboard and make sure your IPv6-public enabled.
|
||||
@@ -18,15 +10,8 @@ Begin with the steps listed in [*Connectivity Test and Configuration*](../nodes/
|
||||
|
||||

|
||||
|
||||
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`
|
||||
- For example Digital Ocean setup isn't the most straight forward, but it's [well documented](https://docs.digitalocean.com/products/networking/ipv6/how-to/enable/#on-existing-droplets) and it works.
|
||||
|
||||
5. Search for guides regarding your particular system and distribution. For Debian based distributions using systemd, some generic guides such as [this one](https://cloudzy.com/blog/configure-ipv6-on-ubuntu/) work as well.
|
||||
|
||||
|
||||
## Other VPS troubleshooting
|
||||
|
||||
### Virtual IPs and hosting via Google & AWS
|
||||
## Virtual IPs and hosting via Google & AWS
|
||||
|
||||
For true internet decentralization we encourage operators to use diverse VPS providers instead of the largest companies offering such services. If for some reasons you have already running AWS or Google and want to setup a `<NODE>` there, please read the following.
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ 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
@@ -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.12.4", default_features = false, features = ["rustls-tls", "json"] }
|
||||
reqwest = { version = "0.11.22", 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"
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
dotenvy = { workspace = true }
|
||||
|
||||
@@ -9,9 +9,12 @@ use crate::mix_node::models::{
|
||||
EconomicDynamicsStats, NodeDescription, NodeStats, SummedDelegations,
|
||||
};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use crate::mix_node::models::{NewModelDescription, OldModelDescription};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use nym_explorer_api_requests::PrettyDetailedMixNodeBond;
|
||||
|
||||
use nym_mixnet_contract_common::{Delegation, MixId};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use rocket::response::status::NotFound;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
@@ -30,18 +33,67 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>,
|
||||
]
|
||||
}
|
||||
|
||||
async fn get_mix_node_description(host: &str, port: u16) -> Result<NodeDescription, ReqwestError> {
|
||||
reqwest::get(format!("http://{host}:{port}/description"))
|
||||
.await?
|
||||
.json::<NodeDescription>()
|
||||
async fn get_mix_node_description(host: &str, port: u16) -> Result<NodeDescription> {
|
||||
let first_url = format!("http://{host}:{port}/description");
|
||||
let first_response = reqwest::get(&first_url).await.context(format!(
|
||||
"Failed to fetch description from nym-mixnode /description url: {}",
|
||||
first_url
|
||||
))?;
|
||||
|
||||
match first_response
|
||||
.error_for_status()
|
||||
.context("Nym-mixnodes /description url returned error status")?
|
||||
.json::<OldModelDescription>()
|
||||
.await
|
||||
{
|
||||
Ok(description) => return Ok(description.into()),
|
||||
Err(e) => log::warn!("Failed to parse old model description: {}", e),
|
||||
}
|
||||
|
||||
let second_url = format!("http://{host}:{port}/api/v1/description");
|
||||
let second_response = reqwest::get(&second_url).await.context(format!(
|
||||
"Failed to fetch description from nym-node /api/v1/description url: {}",
|
||||
second_url
|
||||
))?;
|
||||
|
||||
let description = second_response
|
||||
.error_for_status()
|
||||
.context("Nym-node /api/v1/description url returned error status")?
|
||||
.json::<NewModelDescription>()
|
||||
.await
|
||||
.context("Failed to parse JSON from nym-node /api/v1/description url")?;
|
||||
|
||||
Ok(description.into())
|
||||
}
|
||||
|
||||
async fn get_mix_node_stats(host: &str, port: u16) -> Result<NodeStats, ReqwestError> {
|
||||
reqwest::get(format!("http://{host}:{port}/stats"))
|
||||
.await?
|
||||
pub async fn get_mix_node_stats(host: &str, port: u16) -> Result<NodeStats> {
|
||||
let primary_url = format!("http://{host}:{port}/stats");
|
||||
let secondary_url = format!("http://{host}:{port}/api/v1/metrics/mixing");
|
||||
|
||||
let primary_response = reqwest::get(&primary_url)
|
||||
.await
|
||||
.context("Failed to fetch from primary nym-mixnode /stats url")?;
|
||||
let primary_stats = primary_response
|
||||
.error_for_status()
|
||||
.context("Nym-mixnode url returned error status")?
|
||||
.json::<NodeStats>()
|
||||
.await
|
||||
.context("Failed to parse JSON from primary nym-mixnode /stats url");
|
||||
|
||||
if let Ok(stats) = primary_stats {
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
let secondary_response = reqwest::get(&secondary_url)
|
||||
.await
|
||||
.context("Failed to fetch from nym-node /api/v1/metrics/mixing url")?;
|
||||
let stats = secondary_response
|
||||
.error_for_status()
|
||||
.context("Nym-node /api/v1/metrics/mixing returned error status")?
|
||||
.json::<NodeStats>()
|
||||
.await
|
||||
.context("Failed to parse JSON from nym-node /api/v1/metrics/mixing")?;
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
|
||||
@@ -92,31 +92,86 @@ impl ThreadsafeMixNodeCache {
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub(crate) struct NodeDescription {
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: String,
|
||||
pub(crate) link: String,
|
||||
pub(crate) location: String,
|
||||
pub(crate) name: Option<String>,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) link: Option<String>,
|
||||
pub(crate) location: Option<String>,
|
||||
pub(crate) moniker: Option<String>,
|
||||
pub(crate) website: Option<String>,
|
||||
pub(crate) security_contact: Option<String>,
|
||||
pub(crate) details: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, JsonSchema)]
|
||||
#[derive(Deserialize)]
|
||||
pub struct OldModelDescription {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub link: String,
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewModelDescription {
|
||||
pub moniker: String,
|
||||
pub website: String,
|
||||
pub security_contact: String,
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
impl From<OldModelDescription> for NodeDescription {
|
||||
fn from(old: OldModelDescription) -> Self {
|
||||
NodeDescription {
|
||||
name: Some(old.name),
|
||||
description: Some(old.description),
|
||||
link: Some(old.link),
|
||||
location: Some(old.location),
|
||||
moniker: None,
|
||||
website: None,
|
||||
security_contact: None,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NewModelDescription> for NodeDescription {
|
||||
fn from(new: NewModelDescription) -> Self {
|
||||
NodeDescription {
|
||||
name: None,
|
||||
description: Some(new.details),
|
||||
link: Some(new.website),
|
||||
location: None,
|
||||
moniker: Some(new.moniker),
|
||||
website: None,
|
||||
security_contact: Some(new.security_contact),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, JsonSchema, Debug)]
|
||||
pub(crate) struct NodeStats {
|
||||
#[serde(
|
||||
serialize_with = "humantime_serde::serialize",
|
||||
deserialize_with = "humantime_serde::deserialize"
|
||||
)]
|
||||
update_time: SystemTime,
|
||||
|
||||
#[serde(
|
||||
serialize_with = "humantime_serde::serialize",
|
||||
deserialize_with = "humantime_serde::deserialize"
|
||||
)]
|
||||
previous_update_time: SystemTime,
|
||||
|
||||
#[serde(alias = "received_since_startup")]
|
||||
packets_received_since_startup: u64,
|
||||
#[serde(alias = "sent_since_startup")]
|
||||
packets_sent_since_startup: u64,
|
||||
#[serde(alias = "dropped_since_startup")]
|
||||
packets_explicitly_dropped_since_startup: u64,
|
||||
#[serde(alias = "received_since_last_update")]
|
||||
packets_received_since_last_update: u64,
|
||||
#[serde(alias = "sent_since_last_update")]
|
||||
packets_sent_since_last_update: u64,
|
||||
#[serde(alias = "dropped_since_last_update")]
|
||||
packets_explicitly_dropped_since_last_update: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,11 +0,0 @@
|
||||
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 }
|
||||
@@ -1,34 +0,0 @@
|
||||
// master APIs
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1';
|
||||
export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net';
|
||||
|
||||
export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net';
|
||||
|
||||
export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net';
|
||||
export const 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 || '';
|
||||
@@ -1,173 +0,0 @@
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
GATEWAYS_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
OVERVIEW_API,
|
||||
UPTIME_STORY_API,
|
||||
VALIDATORS_API,
|
||||
SERVICE_PROVIDERS,
|
||||
GATEWAYS_EXPLORER_API,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
CountryDataResponse,
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
MixnodeStatus,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
ValidatorsResponse,
|
||||
Environment,
|
||||
GatewayBondAnnotated,
|
||||
GatewayBond,
|
||||
DirectoryServiceProvider,
|
||||
LocatedGateway,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
const hasExpired = Date.now() - ts > 5000;
|
||||
const curr = localStorage.getItem(key);
|
||||
if (curr && !hasExpired) {
|
||||
return JSON.parse(curr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function storeInCache(key: string, data: any) {
|
||||
localStorage.setItem(key, data);
|
||||
localStorage.setItem('ts', Date.now().toString());
|
||||
}
|
||||
|
||||
export class Api {
|
||||
static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => {
|
||||
const cache = getFromCache('overview-summary');
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
const res = await fetch(`${OVERVIEW_API}/summary`);
|
||||
const json = await res.json();
|
||||
storeInCache('overview-summary', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache('mixnodes');
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
|
||||
const res = await fetch(MIXNODES_API);
|
||||
const json = await res.json();
|
||||
storeInCache('mixnodes', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache(`mixnodes-${status}`);
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
const res = await fetch(`${MIXNODES_API}/active-set/${status}`);
|
||||
const json = await res.json();
|
||||
storeInCache(`mixnodes-${status}`, JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => {
|
||||
const response = await fetch(`${MIXNODE_API}/${id}`);
|
||||
|
||||
// when the mixnode is not found, returned undefined
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
static fetchGateways = async (): Promise<GatewayBond[]> => {
|
||||
const res = await fetch(GATEWAYS_API);
|
||||
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
||||
const res2 = await fetch(GATEWAYS_EXPLORER_API);
|
||||
const locatedGateways: LocatedGateway[] = await res2.json();
|
||||
const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
|
||||
return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
|
||||
...gateway_bond,
|
||||
node_performance,
|
||||
location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
|
||||
}));
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
return json.result;
|
||||
};
|
||||
|
||||
static fetchBlock = async (): Promise<number> => {
|
||||
const res = await fetch(BLOCK_API);
|
||||
const json = await res.json();
|
||||
const { height } = json.result.block.header;
|
||||
return height;
|
||||
};
|
||||
|
||||
static fetchCountryData = async (): Promise<CountryDataResponse> => {
|
||||
const result: CountryDataResponse = {};
|
||||
const res = await fetch(COUNTRY_DATA_API);
|
||||
const json = await res.json();
|
||||
Object.keys(json).forEach((ISO3) => {
|
||||
result[ISO3] = { ISO3, nodes: json[ISO3] };
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations`)).json();
|
||||
|
||||
static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json();
|
||||
|
||||
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/stats`)).json();
|
||||
|
||||
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
|
||||
|
||||
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
|
||||
|
||||
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
|
||||
|
||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
||||
|
||||
static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => {
|
||||
const res = await fetch(SERVICE_PROVIDERS);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
};
|
||||
}
|
||||
|
||||
export const getEnvironment = (): Environment => {
|
||||
const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env;
|
||||
return matchEnv('sandbox') || matchEnv('qa') || 'mainnet';
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }}
|
||||
variant="body1"
|
||||
data-testid="delegation-total-amount"
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Card, CardHeader, CardContent, Typography } from '@mui/material'
|
||||
import React, { ReactEventHandler } from 'react'
|
||||
|
||||
type ContentCardProps = {
|
||||
title?: React.ReactNode
|
||||
subtitle?: string
|
||||
Icon?: React.ReactNode
|
||||
Action?: React.ReactNode
|
||||
errorMsg?: string
|
||||
onClick?: ReactEventHandler
|
||||
}
|
||||
|
||||
export const ContentCard: FCWithChildren<ContentCardProps> = ({
|
||||
title,
|
||||
Icon,
|
||||
Action,
|
||||
subtitle,
|
||||
errorMsg,
|
||||
children,
|
||||
onClick,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
{title && (
|
||||
<CardHeader
|
||||
title={title || ''}
|
||||
avatar={Icon}
|
||||
action={Action}
|
||||
subheader={subtitle}
|
||||
/>
|
||||
)}
|
||||
{children && <CardContent>{children}</CardContent>}
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{errorMsg}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip'
|
||||
|
||||
export const CustomColumnHeading: FCWithChildren<{
|
||||
headingTitle: string
|
||||
tooltipInfo?: string
|
||||
}> = ({ headingTitle, tooltipInfo }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box alignItems="center" display="flex">
|
||||
{tooltipInfo && (
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={headingTitle}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
)}
|
||||
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Button,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose?: () => void;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subTitle?: React.ReactNode | string;
|
||||
confirmButton: React.ReactNode | string;
|
||||
disabled?: boolean;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onConfirm,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subTitle,
|
||||
confirmButton,
|
||||
disabled,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
{title}
|
||||
{subTitle &&
|
||||
(typeof subTitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey">
|
||||
{subTitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subTitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
const ConfirmButton =
|
||||
typeof confirmButton === 'string' ? (
|
||||
<Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}>
|
||||
<Typography variant="button" fontSize="large">
|
||||
{confirmButton}
|
||||
</Typography>
|
||||
</Button>
|
||||
) : (
|
||||
confirmButton
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Button, IconButton } from '@mui/material'
|
||||
import { SxProps } from '@mui/system'
|
||||
import { useIsMobile } from '@/app/hooks'
|
||||
import { DelegateIcon } from '@/app/icons/DelevateSVG'
|
||||
|
||||
export const DelegateIconButton: FCWithChildren<{
|
||||
size?: 'small' | 'medium'
|
||||
disabled?: boolean
|
||||
tooltip?: React.ReactNode
|
||||
sx?: SxProps
|
||||
onDelegate: () => void
|
||||
}> = ({ onDelegate, sx, disabled, size = 'medium' }) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const handleOnDelegate = () => {
|
||||
onDelegate()
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<IconButton size="small" disabled={disabled} onClick={handleOnDelegate}>
|
||||
<DelegateIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
onClick={handleOnDelegate}
|
||||
sx={sx}
|
||||
>
|
||||
Delegate
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Box, SxProps } from '@mui/material'
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'
|
||||
import { CurrencyDenom, DecCoin } from '@nymproject/types'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
import { urls } from '@/app/utils'
|
||||
import { useDelegationsContext } from '@/app/context/delegations'
|
||||
import { validateAmount } from '@/app/utils/currency'
|
||||
import { SimpleModal } from './SimpleModal'
|
||||
import { ModalListItem } from './ModalListItem'
|
||||
import { DelegationModalProps } from './DelegationModal'
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = 10
|
||||
|
||||
type Props = {
|
||||
mixId: number
|
||||
identityKey: string
|
||||
header?: string
|
||||
buttonText?: string
|
||||
rewardInterval?: string
|
||||
estimatedReward?: number
|
||||
profitMarginPercentage?: string | null
|
||||
nodeUptimePercentage?: number | null
|
||||
denom: CurrencyDenom
|
||||
sx?: SxProps
|
||||
backdropProps?: object
|
||||
onClose: () => void
|
||||
onOk?: (delegationModalProps: DelegationModalProps) => void
|
||||
}
|
||||
|
||||
export const DelegateModal = ({
|
||||
mixId,
|
||||
identityKey,
|
||||
onClose,
|
||||
onOk,
|
||||
denom,
|
||||
sx,
|
||||
}: Props) => {
|
||||
const [amount, setAmount] = useState<DecCoin | undefined>({
|
||||
amount: '10',
|
||||
denom: 'nym',
|
||||
})
|
||||
const [isValidated, setValidated] = useState<boolean>(false)
|
||||
const [errorAmount, setErrorAmount] = useState<string | undefined>()
|
||||
|
||||
const { address, balance } = useWalletContext()
|
||||
const { handleDelegate } = useDelegationsContext()
|
||||
|
||||
const validate = async () => {
|
||||
let newValidatedValue = true
|
||||
let errorAmountMessage
|
||||
|
||||
if (amount && !(await validateAmount(amount.amount, '0'))) {
|
||||
newValidatedValue = false
|
||||
errorAmountMessage = 'Please enter a valid amount'
|
||||
}
|
||||
|
||||
if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) {
|
||||
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
if (!amount?.amount.length) {
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
if (amount && balance.data && +balance.data - +amount.amount <= 0) {
|
||||
errorAmountMessage = 'Not enough funds'
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
setErrorAmount(errorAmountMessage)
|
||||
setValidated(newValidatedValue)
|
||||
}
|
||||
|
||||
const delegateToMixnode = async ({
|
||||
delegationMixId,
|
||||
delegationAmount,
|
||||
}: {
|
||||
delegationMixId: number
|
||||
delegationAmount: string
|
||||
}) => {
|
||||
try {
|
||||
const tx = await handleDelegate(delegationMixId, delegationAmount)
|
||||
return tx
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate to mixnode', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (mixId && amount && onOk) {
|
||||
onOk({
|
||||
status: 'loading',
|
||||
})
|
||||
try {
|
||||
if (!address) {
|
||||
throw new Error('Please connect your wallet')
|
||||
}
|
||||
|
||||
const tx = await delegateToMixnode({
|
||||
delegationMixId: mixId,
|
||||
delegationAmount: amount.amount,
|
||||
})
|
||||
|
||||
if (!tx) {
|
||||
throw new Error('Failed to delegate')
|
||||
}
|
||||
|
||||
onOk({
|
||||
status: 'success',
|
||||
message: 'Delegation can take up to one hour to process',
|
||||
transactions: [
|
||||
{
|
||||
url: `${urls('MAINNET').blockExplorer}/transaction/${
|
||||
tx.transactionHash
|
||||
}`,
|
||||
hash: tx.transactionHash,
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate', e)
|
||||
onOk({
|
||||
status: 'error',
|
||||
message: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAmountChanged = (newAmount: DecCoin) => {
|
||||
setAmount(newAmount)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
validate()
|
||||
}, [amount, identityKey, mixId])
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
onClose={onClose}
|
||||
onOk={handleConfirm}
|
||||
header="Delegate"
|
||||
okLabel="Delegate"
|
||||
okDisabled={!isValidated}
|
||||
sx={sx}
|
||||
>
|
||||
<Box sx={{ mt: 3 }} gap={2}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Node identity key"
|
||||
onChanged={() => undefined}
|
||||
initialValue={identityKey}
|
||||
readOnly
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}>
|
||||
<CurrencyFormField
|
||||
showCoinMark={false}
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
label="Amount"
|
||||
initialValue={amount?.amount || '10'}
|
||||
onChanged={handleAmountChanged}
|
||||
denom={denom}
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<ModalListItem
|
||||
label="Account balance"
|
||||
value={`${balance.data} NYM`}
|
||||
divider
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" />
|
||||
</SimpleModal>
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Typography, SxProps, Stack } from '@mui/material'
|
||||
import { Link } from '@nymproject/react/link/Link'
|
||||
import { LoadingModal } from './LoadingModal'
|
||||
import { ConfirmationModal } from './ConfirmationModal'
|
||||
import { ErrorModal } from './ErrorModal'
|
||||
|
||||
export type DelegationModalProps = {
|
||||
status: 'loading' | 'success' | 'error' | 'info'
|
||||
message?: string
|
||||
transactions?: {
|
||||
url: string
|
||||
hash: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const DelegationModal: FCWithChildren<
|
||||
DelegationModalProps & {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
sx?: SxProps
|
||||
backdropProps?: object
|
||||
children?: React.ReactNode
|
||||
}
|
||||
> = ({
|
||||
status,
|
||||
message,
|
||||
transactions,
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
sx,
|
||||
backdropProps,
|
||||
}) => {
|
||||
if (status === 'loading')
|
||||
return <LoadingModal sx={sx} backdropProps={backdropProps} />
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<ErrorModal message={message} sx={sx} open={open} onClose={onClose}>
|
||||
{children}
|
||||
</ErrorModal>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'info') {
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={open}
|
||||
title="Connect wallet"
|
||||
confirmButton="OK"
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<Typography>{message}</Typography>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={open}
|
||||
onConfirm={onClose || (() => {})}
|
||||
title="Transaction successful"
|
||||
confirmButton="Done"
|
||||
>
|
||||
<Stack alignItems="center" spacing={2} mb={0}>
|
||||
{message && <Typography>{message}</Typography>}
|
||||
{transactions?.length === 1 && (
|
||||
<Link
|
||||
href={transactions[0].url}
|
||||
target="_blank"
|
||||
sx={{ ml: 1 }}
|
||||
text="View on blockchain"
|
||||
noIcon
|
||||
/>
|
||||
)}
|
||||
{transactions && transactions.length > 1 && (
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Typography>View the transactions on blockchain:</Typography>
|
||||
{transactions.map(({ url, hash }) => (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
sx={{ ml: 1 }}
|
||||
text={hash.slice(0, 6)}
|
||||
key={hash}
|
||||
noIcon
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
|
||||
import { modalStyle } from './SimpleModal';
|
||||
|
||||
export const ErrorModal: FCWithChildren<{
|
||||
open: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
onClose: () => void;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, open, title, message, sx, backdropProps, onClose }) => (
|
||||
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
|
||||
<Box sx={{ ...modalStyle(), ...sx }} textAlign="center">
|
||||
<Typography color={(theme) => theme.palette.error.main} mb={1}>
|
||||
{title || 'Oh no! Something went wrong...'}
|
||||
</Typography>
|
||||
<Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}>
|
||||
{message}
|
||||
</Typography>
|
||||
{children}
|
||||
<Button variant="contained" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
import { modalStyle } from './SimpleModal';
|
||||
|
||||
export const LoadingModal: FCWithChildren<{
|
||||
text?: string;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
}> = ({ sx, text = 'Please wait...' }) => (
|
||||
<Modal open>
|
||||
<Box sx={{ ...modalStyle(), ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>{text}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, SxProps } from '@mui/material';
|
||||
|
||||
export const ModalDivider: FCWithChildren<{
|
||||
sx?: SxProps;
|
||||
}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material';
|
||||
import { ModalDivider } from './ModalDivider';
|
||||
|
||||
export const ModalListItem: FCWithChildren<{
|
||||
label: string;
|
||||
divider?: boolean;
|
||||
hidden?: boolean;
|
||||
fontWeight?: TypographyProps['fontWeight'];
|
||||
fontSize?: TypographyProps['fontSize'];
|
||||
light?: boolean;
|
||||
value?: React.ReactNode;
|
||||
sxValue?: SxProps;
|
||||
}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => (
|
||||
<Box sx={{ display: hidden ? 'none' : 'block' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{value && (
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={fontWeight}
|
||||
sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{divider && <ModalDivider />}
|
||||
</Box>
|
||||
);
|
||||
@@ -1,152 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import ErrorOutline from '@mui/icons-material/ErrorOutline'
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export const modalStyle = (width: number | string = 600) => ({
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
})
|
||||
|
||||
export const StyledBackButton = ({
|
||||
onBack,
|
||||
label,
|
||||
fullWidth,
|
||||
sx,
|
||||
}: {
|
||||
onBack: () => void
|
||||
label?: string
|
||||
fullWidth?: boolean
|
||||
sx?: SxProps
|
||||
}) => (
|
||||
<Button
|
||||
disableFocusRipple
|
||||
size="large"
|
||||
fullWidth={fullWidth}
|
||||
variant="outlined"
|
||||
onClick={onBack}
|
||||
sx={sx}
|
||||
>
|
||||
{label || <ArrowBackIosNewIcon fontSize="small" />}
|
||||
</Button>
|
||||
)
|
||||
|
||||
export const SimpleModal: FCWithChildren<{
|
||||
open: boolean
|
||||
hideCloseIcon?: boolean
|
||||
displayErrorIcon?: boolean
|
||||
displayInfoIcon?: boolean
|
||||
headerStyles?: SxProps
|
||||
subHeaderStyles?: SxProps
|
||||
buttonFullWidth?: boolean
|
||||
onClose?: () => void
|
||||
onOk?: () => Promise<void>
|
||||
onBack?: () => void
|
||||
header: string | React.ReactNode
|
||||
subHeader?: string
|
||||
okLabel: string
|
||||
backLabel?: string
|
||||
backButtonFullWidth?: boolean
|
||||
okDisabled?: boolean
|
||||
sx?: SxProps
|
||||
children?: React.ReactNode
|
||||
}> = ({
|
||||
open,
|
||||
hideCloseIcon,
|
||||
displayErrorIcon,
|
||||
displayInfoIcon,
|
||||
headerStyles,
|
||||
buttonFullWidth,
|
||||
onClose,
|
||||
okDisabled,
|
||||
onOk,
|
||||
onBack,
|
||||
header,
|
||||
subHeader,
|
||||
okLabel,
|
||||
backLabel,
|
||||
backButtonFullWidth,
|
||||
sx,
|
||||
children,
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
|
||||
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{typeof header === 'string' ? (
|
||||
<Typography
|
||||
fontSize={20}
|
||||
fontWeight={600}
|
||||
sx={{ color: 'text.primary', ...headerStyles }}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
mt={subHeader ? 0.5 : 0}
|
||||
mb={3}
|
||||
fontSize={12}
|
||||
color={(theme) => theme.palette.text.secondary}
|
||||
>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
|
||||
{children}
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
width: buttonFullWidth ? '100%' : null,
|
||||
}}
|
||||
>
|
||||
{onBack && (
|
||||
<StyledBackButton
|
||||
onBack={onBack}
|
||||
label={backLabel}
|
||||
fullWidth={backButtonFullWidth}
|
||||
/>
|
||||
)}
|
||||
{onOk && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={onOk}
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export * from './ConfirmationModal';
|
||||
export * from './DelegateIconButton';
|
||||
export * from './DelegationModal';
|
||||
export * from './DelegateModal';
|
||||
export * from './ErrorModal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ModalDivider';
|
||||
export * from './ModalListItem';
|
||||
export * from './SimpleModal';
|
||||
export * from './styles';
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
export const backDropStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return {
|
||||
style: {
|
||||
left: mode === 'light' ? '0' : '50%',
|
||||
width: '50%',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const modalStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '25%' : '75%' };
|
||||
};
|
||||
|
||||
export const dialogStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '-50%' : '50%' };
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Link,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCellProps,
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip'
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
|
||||
import { Box } from '@mui/system'
|
||||
import { unymToNym } from '@/app/utils/currency'
|
||||
import { GatewayEnrichedRowType } from './Gateways/Gateways'
|
||||
import { MixnodeRowType } from './MixNodes'
|
||||
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
Slider,
|
||||
Typography,
|
||||
Box,
|
||||
Snackbar,
|
||||
Slide,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import {
|
||||
MixnodeStatusWithAll,
|
||||
toMixnodeStatus,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { EnumFilterKey, TFilterItem, TFilters } from '@/app/typeDefs/filters'
|
||||
import { Api } from '@/app/api'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { formatOnSave, generateFilterSchema } from './filterSchema'
|
||||
import FiltersButton from './FiltersButton'
|
||||
|
||||
const FilterItem = ({
|
||||
label,
|
||||
id,
|
||||
tooltipInfo,
|
||||
value,
|
||||
isSmooth,
|
||||
marks,
|
||||
scale,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
}: TFilterItem & {
|
||||
onChange: (id: EnumFilterKey, newValue: number[]) => void
|
||||
}) => (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography gutterBottom>{label}</Typography>
|
||||
<Typography fontSize={12}>{tooltipInfo}</Typography>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(e: Event, newValue: number | number[]) =>
|
||||
onChange(id, newValue as number[])
|
||||
}
|
||||
valueLabelDisplay={isSmooth ? 'auto' : 'off'}
|
||||
marks={marks}
|
||||
step={isSmooth ? 1 : null}
|
||||
scale={scale}
|
||||
min={min}
|
||||
max={max}
|
||||
valueLabelFormat={(val: number) =>
|
||||
val === 100 && id === 'stakeSaturation' ? '>100' : val
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const Filters = () => {
|
||||
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext()
|
||||
const { status } = useParams<{
|
||||
status: 'active' | 'standby' | 'inactive' | 'all'
|
||||
}>()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
const [isFiltered, setIsFiltered] = useState(false)
|
||||
const [filters, setFilters] = React.useState<TFilters>()
|
||||
const [upperSaturationValue, setUpperSaturationValue] =
|
||||
React.useState<number>(100)
|
||||
|
||||
const baseFilters = useRef<TFilters>()
|
||||
const prevFilters = useRef<TFilters>()
|
||||
|
||||
const handleToggleShowFilters = () => setShowFilters(!showFilters)
|
||||
|
||||
const initialiseFilters = useCallback(async () => {
|
||||
const allMixnodes = await Api.fetchMixnodes()
|
||||
if (allMixnodes) {
|
||||
setUpperSaturationValue(
|
||||
Math.round(
|
||||
Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1
|
||||
)
|
||||
)
|
||||
const initFilters = generateFilterSchema()
|
||||
baseFilters.current = initFilters
|
||||
prevFilters.current = initFilters
|
||||
setFilters(initFilters)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOnChange = (id: EnumFilterKey, newValue: number[]) => {
|
||||
if (id === 'stakeSaturation' && newValue[1] === 100) {
|
||||
newValue.splice(1, 1, upperSaturationValue)
|
||||
}
|
||||
setFilters((ftrs) => {
|
||||
if (ftrs)
|
||||
return {
|
||||
...ftrs,
|
||||
[id]: {
|
||||
...ftrs[id],
|
||||
value: newValue,
|
||||
},
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleOnSave = async () => {
|
||||
setShowFilters(false)
|
||||
await filterMixnodes(formatOnSave(filters!), status)
|
||||
setIsFiltered(true)
|
||||
prevFilters.current = filters
|
||||
}
|
||||
|
||||
const handleOnCancel = () => {
|
||||
setShowFilters(false)
|
||||
setFilters(prevFilters.current)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters(baseFilters.current)
|
||||
setIsFiltered(false)
|
||||
prevFilters.current = baseFilters.current
|
||||
}
|
||||
|
||||
const onClearFilters = async () => {
|
||||
await fetchMixnodes(toMixnodeStatus(MixnodeStatusWithAll[status]))
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initialiseFilters()
|
||||
}, [initialiseFilters])
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters()
|
||||
}, [status])
|
||||
|
||||
if (!filters) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar
|
||||
open={isFiltered}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
message="Filters applied"
|
||||
TransitionComponent={Slide}
|
||||
transitionDuration={250}
|
||||
>
|
||||
<Alert
|
||||
severity="info"
|
||||
variant={isMobile ? 'standard' : 'outlined'}
|
||||
sx={{ color: (t) => t.palette.info.light }}
|
||||
action={
|
||||
<Button size="small" onClick={onClearFilters}>
|
||||
CLEAR FILTERS
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{mixnodes?.data?.length} mixnodes matched your criteria
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<FiltersButton onClick={handleToggleShowFilters} fullWidth />
|
||||
<Dialog
|
||||
open={showFilters}
|
||||
onClose={handleToggleShowFilters}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Mixnode filters</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{Object.values(filters).map((v) => (
|
||||
<FilterItem {...v} key={v.id} onChange={handleOnChange} />
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button size="large" onClick={handleOnCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" size="large" onClick={handleOnSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton } from '@mui/material';
|
||||
import { Tune } from '@mui/icons-material';
|
||||
|
||||
type FiltersButtonProps = {
|
||||
iconOnly?: boolean;
|
||||
fullWidth?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<IconButton onClick={onClick} color="primary">
|
||||
<Tune />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth={fullWidth}
|
||||
size="large"
|
||||
variant="contained"
|
||||
endIcon={<Tune />}
|
||||
onClick={onClick}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FiltersButton;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { EnumFilterKey, TFilters } from '../../typeDefs/filters';
|
||||
|
||||
export const generateFilterSchema = () => ({
|
||||
profitMargin: {
|
||||
label: 'Profit margin (%)',
|
||||
id: EnumFilterKey.profitMargin,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo:
|
||||
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
|
||||
},
|
||||
stakeSaturation: {
|
||||
label: 'Stake saturation (%)',
|
||||
id: EnumFilterKey.stakeSaturation,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({
|
||||
value: value < 100 ? value : 100,
|
||||
label: value < 100 ? value : '>100',
|
||||
})),
|
||||
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
|
||||
},
|
||||
routingScore: {
|
||||
label: 'Routing score (%)',
|
||||
id: EnumFilterKey.routingScore,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards',
|
||||
},
|
||||
});
|
||||
|
||||
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = value_1 / 100;
|
||||
const upperValue = value_2 / 100;
|
||||
|
||||
return [lowerValue, upperValue];
|
||||
};
|
||||
|
||||
export const formatOnSave = (filters: TFilters) => ({
|
||||
routingScore: filters.routingScore.value,
|
||||
profitMargin: filters.profitMargin.value,
|
||||
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import MuiLink from '@mui/material/Link'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useIsMobile } from '../hooks/useIsMobile'
|
||||
import { NymVpnIcon } from '../icons/NymVpn'
|
||||
import { Socials } from './Socials'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const Footer: FCWithChildren = () => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
mt: 3,
|
||||
pt: 3,
|
||||
pb: 3,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box marginRight={1}>
|
||||
<Link href="http://nymvpn.com" target="_blank">
|
||||
<NymVpnIcon />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Socials isFooter />
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
textAlign: isMobile ? 'center' : 'end',
|
||||
color: 'nym.muted.onDarkBg',
|
||||
}}
|
||||
>
|
||||
© {new Date().getFullYear()} Nym Technologies SA, all rights reserved
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react'
|
||||
import { FormControl, MenuItem, Select } from '@mui/material'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export enum VersionSelectOptions {
|
||||
latestVersion = 'Latest versions',
|
||||
olderVersions = 'Older versions',
|
||||
all = 'All',
|
||||
}
|
||||
export const VersionDisplaySelector = ({
|
||||
selected,
|
||||
handleChange,
|
||||
}: {
|
||||
selected: VersionSelectOptions
|
||||
handleChange: (option: VersionSelectOptions) => void
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<FormControl size="small">
|
||||
<Select
|
||||
value={selected}
|
||||
onChange={(e) => handleChange(e.target.value as VersionSelectOptions)}
|
||||
labelId="simple-select-label"
|
||||
id="simple-select"
|
||||
sx={{
|
||||
marginRight: isMobile ? 0 : 2,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.latestVersion}
|
||||
data-testid="show-gateway-latest-version"
|
||||
>
|
||||
{VersionSelectOptions.latestVersion}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.olderVersions}
|
||||
data-testid="show-gateway-old-versions"
|
||||
>
|
||||
{VersionSelectOptions.olderVersions}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.all}
|
||||
data-testid="show-gateway-all-versions"
|
||||
>
|
||||
{VersionSelectOptions.all}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
|
||||
import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined';
|
||||
import { MixnodeStatus } from '../typeDefs/explorer-api';
|
||||
|
||||
export const Icons = {
|
||||
Mixnodes: {
|
||||
Status: {
|
||||
Active: CheckCircleOutlineIcon,
|
||||
Standby: PauseCircleOutlineIcon,
|
||||
Inactive: CircleOutlinedIcon,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getMixNodeIcon = (value: any) => {
|
||||
if (value && typeof value === 'string') {
|
||||
switch (value) {
|
||||
case MixnodeStatus.active:
|
||||
return Icons.Mixnodes.Status.Active;
|
||||
case MixnodeStatus.standby:
|
||||
return Icons.Mixnodes.Status.Standby;
|
||||
default:
|
||||
return Icons.Mixnodes.Status.Inactive;
|
||||
}
|
||||
}
|
||||
return Icons.Mixnodes.Status.Inactive;
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Alert, Box, CircularProgress, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import { ExpandMore } from '@mui/icons-material'
|
||||
import { currencyToString } from '@/app/utils/currency'
|
||||
import { useMixnodeContext } from '@/app/context/mixnode'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export const BondBreakdownTable: FCWithChildren = () => {
|
||||
const { mixNode, delegations, uniqDelegations } = useMixnodeContext()
|
||||
const [showDelegations, toggleShowDelegations] =
|
||||
React.useState<boolean>(false)
|
||||
|
||||
const [bonds, setBonds] = React.useState({
|
||||
delegations: '0',
|
||||
pledges: '0',
|
||||
bondsTotal: '0',
|
||||
hasLoaded: false,
|
||||
})
|
||||
const theme = useTheme()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mixNode?.data) {
|
||||
// delegations
|
||||
const decimalisedDelegations = currencyToString({
|
||||
amount: mixNode.data.total_delegation.amount.toString(),
|
||||
denom: mixNode.data.total_delegation.denom,
|
||||
})
|
||||
|
||||
// pledges
|
||||
const decimalisedPledges = currencyToString({
|
||||
amount: mixNode.data.pledge_amount.amount.toString(),
|
||||
denom: mixNode.data.pledge_amount.denom,
|
||||
})
|
||||
|
||||
// bonds total (del + pledges)
|
||||
const pledgesSum = Number(mixNode.data.pledge_amount.amount)
|
||||
const delegationsSum = Number(mixNode.data.total_delegation.amount)
|
||||
const bondsTotal = currencyToString({
|
||||
amount: (pledgesSum + delegationsSum).toString(),
|
||||
})
|
||||
|
||||
setBonds({
|
||||
delegations: decimalisedDelegations,
|
||||
pledges: decimalisedPledges,
|
||||
bondsTotal,
|
||||
hasLoaded: true,
|
||||
})
|
||||
}
|
||||
}, [mixNode])
|
||||
|
||||
const expandDelegations = () => {
|
||||
if (delegations?.data && delegations.data.length > 0) {
|
||||
toggleShowDelegations(!showDelegations)
|
||||
}
|
||||
}
|
||||
const calcBondPercentage = (num: number) => {
|
||||
if (mixNode?.data) {
|
||||
const rawDelegationAmount = Number(mixNode.data.total_delegation.amount)
|
||||
const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount)
|
||||
const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount
|
||||
return ((num * 100) / rawTotalBondsAmount).toFixed(1)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (mixNode?.isLoading || delegations?.isLoading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
if (mixNode?.error) {
|
||||
return <Alert severity="error">Mixnode not found</Alert>
|
||||
}
|
||||
if (delegations?.error) {
|
||||
return <Alert severity="error">Unable to get delegations for mixnode</Alert>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
|
||||
<TableBody>
|
||||
<TableRow sx={isMobile ? { minWidth: '70vw' } : null}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
width: '150px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Stake total
|
||||
</TableCell>
|
||||
<TableCell align="left" data-testid="bond-total-amount">
|
||||
{bonds.bondsTotal}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Bond</TableCell>
|
||||
<TableCell align="left" data-testid="pledge-total-amount">
|
||||
{bonds.pledges}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell onClick={expandDelegations} align="left">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Delegation total {'\u00A0'}
|
||||
{delegations?.data && delegations?.data?.length > 0 && (
|
||||
<ExpandMore />
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="left" data-testid="delegation-total-amount">
|
||||
{bonds.delegations}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{showDelegations && (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 400,
|
||||
overflowY: 'scroll',
|
||||
p: 2,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
width: '100%',
|
||||
p: 2,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
data-testid="delegations-total-amount"
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Delegations
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Delegators
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Amount
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
width: '200px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Share of stake
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{uniqDelegations?.data?.map(({ owner, amount: { amount } }) => (
|
||||
<TableRow key={owner}>
|
||||
<TableCell sx={isMobile ? { width: 190 } : null} align="left">
|
||||
{owner}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{currencyToString({ amount: amount.toString() })}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{calcBondPercentage(amount)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, Button, Grid, Typography, useTheme } from '@mui/material'
|
||||
import Identicon from 'react-identicons'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { MixNodeDescriptionResponse } from '@/app/typeDefs/explorer-api'
|
||||
import { getMixNodeStatusText, MixNodeStatus } from './Status'
|
||||
import { MixnodeRowType } from '.'
|
||||
|
||||
interface MixNodeDetailProps {
|
||||
mixNodeRow: MixnodeRowType
|
||||
mixnodeDescription: MixNodeDescriptionResponse
|
||||
}
|
||||
|
||||
export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({
|
||||
mixNodeRow,
|
||||
mixnodeDescription,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const palette = [theme.palette.text.primary]
|
||||
const isMobile = useIsMobile()
|
||||
const statusText = React.useMemo(
|
||||
() => getMixNodeStatusText(mixNodeRow.status),
|
||||
[mixNodeRow.status]
|
||||
)
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
width="100%"
|
||||
>
|
||||
<Box
|
||||
width={72}
|
||||
height={72}
|
||||
sx={{
|
||||
minWidth: 72,
|
||||
minHeight: 72,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.palette.text.primary,
|
||||
borderStyle: 'solid',
|
||||
borderRadius: '50%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Identicon
|
||||
size={43}
|
||||
string={mixNodeRow.identity_key}
|
||||
palette={palette}
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}>
|
||||
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
|
||||
<Typography>
|
||||
{(mixnodeDescription.description || '').slice(0, 1000)}
|
||||
</Typography>
|
||||
<Button
|
||||
component="a"
|
||||
variant="text"
|
||||
sx={{
|
||||
mt: isMobile ? 2 : 4,
|
||||
borderRadius: '30px',
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
}}
|
||||
href={mixnodeDescription.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
maxWidth="250px"
|
||||
>
|
||||
{mixnodeDescription.link}
|
||||
</Typography>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={6}
|
||||
display="flex"
|
||||
justifyContent={isMobile ? 'start' : 'end'}
|
||||
mt={isMobile ? 3 : undefined}
|
||||
>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography
|
||||
fontWeight="600"
|
||||
alignSelf={isMobile ? 'start' : 'self-end'}
|
||||
>
|
||||
Node status:
|
||||
</Typography>
|
||||
<Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||
<MixNodeStatus status={mixNodeRow.status} />
|
||||
</Box>
|
||||
<Typography
|
||||
mt={1}
|
||||
alignSelf={isMobile ? 'start' : 'self-end'}
|
||||
color={theme.palette.text.secondary}
|
||||
fontSize="smaller"
|
||||
>
|
||||
This node is {statusText} in this epoch
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ColumnsType } from '../../DetailTable';
|
||||
|
||||
export const EconomicsInfoColumns: ColumnsType[] = [
|
||||
{
|
||||
field: 'estimatedTotalReward',
|
||||
title: 'Estimated Total Reward',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
|
||||
},
|
||||
{
|
||||
field: 'estimatedOperatorReward',
|
||||
title: 'Estimated Operator Reward',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
|
||||
},
|
||||
{
|
||||
field: 'selectionChance',
|
||||
title: 'Active Set Probability',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
|
||||
},
|
||||
{
|
||||
field: 'profitMargin',
|
||||
title: 'Profit Margin',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
|
||||
},
|
||||
{
|
||||
field: 'operatingCost',
|
||||
title: 'Operating Cost',
|
||||
width: '10%',
|
||||
tooltipInfo:
|
||||
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
|
||||
},
|
||||
{
|
||||
field: 'nodePerformance',
|
||||
title: 'Routing Score',
|
||||
width: '10%',
|
||||
tooltipInfo:
|
||||
"Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.",
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
tooltipInfo: "Mixnode's average routing score in the last 24 hour",
|
||||
},
|
||||
];
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics/ProgressBar',
|
||||
component: EconomicsProgress,
|
||||
} as ComponentMeta<typeof EconomicsProgress>;
|
||||
|
||||
const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {};
|
||||
|
||||
export const OverThreshold = Template.bind({});
|
||||
OverThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 120,
|
||||
};
|
||||
|
||||
export const UnderThreshold = Template.bind({});
|
||||
UnderThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 80,
|
||||
};
|
||||
|
||||
export const OnThreshold = Template.bind({});
|
||||
OnThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 100,
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
const parseToNumber = (value: number | undefined | string) =>
|
||||
typeof value === 'string' ? parseInt(value || '', 10) : value || 0;
|
||||
|
||||
export const EconomicsProgress: FCWithChildren<
|
||||
LinearProgressProps & {
|
||||
threshold?: number;
|
||||
color: string;
|
||||
}
|
||||
> = ({ threshold, color, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const { value } = props;
|
||||
|
||||
const valueNumber: number = parseToNumber(value);
|
||||
const thresholdNumber: number = parseToNumber(threshold);
|
||||
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 6 / 10,
|
||||
color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
|
||||
}}
|
||||
>
|
||||
<LinearProgress
|
||||
{...props}
|
||||
variant="determinate"
|
||||
color={color}
|
||||
value={percentageToDisplay}
|
||||
sx={{ width: '100%', borderRadius: '5px' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { DelegatorsInfoTable } from './Table';
|
||||
import { EconomicsInfoColumns } from './Columns';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics',
|
||||
component: DelegatorsInfoTable,
|
||||
} as ComponentMeta<typeof DelegatorsInfoTable>;
|
||||
|
||||
const row: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: 'High',
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '10 %',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '11121 NYM',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
},
|
||||
};
|
||||
|
||||
const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Good',
|
||||
},
|
||||
};
|
||||
|
||||
const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Low',
|
||||
},
|
||||
};
|
||||
|
||||
const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '-',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '-',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
rows: [emptyRow],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceHigh = Template.bind({});
|
||||
selectionChanceHigh.args = {
|
||||
rows: [row],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceGood = Template.bind({});
|
||||
selectionChanceGood.args = {
|
||||
rows: [rowGoodProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceLow = Template.bind({});
|
||||
selectionChanceLow.args = {
|
||||
rows: [rowLowProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { currencyToString, unymToNym } from '@/app/utils/currency';
|
||||
import { useMixnodeContext } from '@/app/context/mixnode';
|
||||
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '@/app/typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '@/app/utils';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) =>
|
||||
economicDynamicsStats?.data?.active_set_inclusion_probability || '-';
|
||||
|
||||
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
const { economicDynamicsStats, mixNode } = useMixnodeContext();
|
||||
|
||||
const estimatedNodeRewards =
|
||||
currencyToString({
|
||||
amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '',
|
||||
}) || '-';
|
||||
const estimatedOperatorRewards =
|
||||
currencyToString({
|
||||
amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '',
|
||||
}) || '-';
|
||||
const profitMargin = mixNode?.data?.profit_margin_percent
|
||||
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
|
||||
: '-';
|
||||
const avgUptime = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.last_24h)
|
||||
: '-';
|
||||
const nodePerformance = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.most_recent)
|
||||
: '-';
|
||||
|
||||
const opCost = mixNode?.data?.operating_cost;
|
||||
|
||||
return {
|
||||
id: 1,
|
||||
estimatedTotalReward: {
|
||||
value: estimatedNodeRewards,
|
||||
},
|
||||
estimatedOperatorReward: {
|
||||
value: estimatedOperatorRewards,
|
||||
},
|
||||
selectionChance: {
|
||||
value: selectionChance(economicDynamicsStats),
|
||||
},
|
||||
profitMargin: {
|
||||
value: profitMargin ? `${profitMargin} %` : '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: avgUptime ? `${avgUptime} %` : '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: nodePerformance,
|
||||
},
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user