Compare commits

..

3 Commits

Author SHA1 Message Date
Sachin Kamath 1bdcf9c3cf fix review comments 2024-11-06 14:16:13 +05:30
Sachin Kamath 4ebb9cd239 clippy 2024-11-05 16:01:18 +05:30
Sachin Kamath 620d68ea2f nyxd-scraper: add config to make pre-commit storage optional 2024-11-05 15:49:30 +05:30
186 changed files with 10903 additions and 9305 deletions
@@ -0,0 +1,45 @@
name: ci-nym-credential-proxy
on:
pull_request:
paths:
- 'common/**'
- 'nym-credential-proxy/**'
- '.github/workspace/ci-nym-credential-proxy.yml'
workflow_dispatch:
jobs:
build:
runs-on: arc-ubuntu-22.04
env:
CARGO_TERM_COLOR: always
MANIFEST_PATH: "--manifest-path nym-credential-proxy/Cargo.toml"
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: ${{ env.MANIFEST_PATH }} --all -- --check
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
Generated
+719 -450
View File
File diff suppressed because it is too large Load Diff
+23 -24
View File
@@ -19,33 +19,33 @@ members = [
"clients/native",
"clients/native/websocket-requests",
"clients/socks5",
"common/async-file-watcher",
"common/authenticator-requests",
"common/async-file-watcher",
"common/bandwidth-controller",
"common/bin-common",
"common/client-core",
"common/client-core/config-types",
"common/client-core/gateways-storage",
"common/client-core/surb-storage",
"common/client-core/gateways-storage",
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/country-group",
"common/credential-storage",
"common/credential-utils",
"common/credential-verification",
"common/credentials",
"common/credential-utils",
"common/credentials-interface",
"common/credential-verification",
"common/crypto",
"common/dkg",
"common/ecash-double-spending",
@@ -65,10 +65,10 @@ members = [
"common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nym_offline_compact_ecash",
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymcoconut",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -104,23 +104,20 @@ members = [
"gateway",
"integrations/bity",
"mixnode",
"sdk/ffi/cpp",
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/lib/socks5-listener",
"sdk/rust/nym-sdk",
"sdk/ffi/shared",
"sdk/ffi/go",
"sdk/ffi/cpp",
"service-providers/authenticator",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"nym-api",
"nym-api/nym-api-requests",
"nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-credential-proxy/vpn-api-lib-wasm",
"nym-data-observatory",
"nym-network-monitor",
"nym-api",
"nym-browser-extension/storage",
"nym-api/nym-api-requests",
"nym-data-observatory",
"nym-node",
"nym-node/nym-node-http-api",
"nym-node/nym-node-requests",
@@ -143,11 +140,11 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
]
default-members = [
@@ -158,7 +155,6 @@ default-members = [
"gateway",
"mixnode",
"nym-api",
"nym-credential-proxy/nym-credential-proxy",
"nym-data-observatory",
"nym-node",
"nym-node-status-api",
@@ -197,14 +193,16 @@ aead = "0.5.2"
anyhow = "1.0.90"
argon2 = "0.5.0"
async-trait = "0.1.83"
axum-client-ip = "0.6.1"
axum = "0.7.5"
axum-extra = "0.9.4"
base64 = "0.22.1"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
# can we unify those?
bit-vec = "0.7.0"
bitvec = "1.0.0"
blake3 = "1.5.4"
bloomfilter = "1.0.14"
bs58 = "0.5.1"
@@ -271,7 +269,7 @@ ipnetwork = "0.20"
isocountry = "0.3.2"
itertools = "0.13.0"
k256 = "0.13"
lazy_static = "1.5.0"
lazy_static = "1.4.0"
ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
@@ -281,7 +279,7 @@ moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
notify = "5.1.0"
okapi = "0.7.0"
once_cell = "1.20.2"
once_cell = "1.7.2"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
parking_lot = "0.12.3"
@@ -409,6 +407,7 @@ wasm-bindgen-futures = "0.4.45"
wasmtimer = "0.2.0"
web-sys = "0.3.72"
# Profile settings for individual crates
# Compile-time verified queries do quite a bit of work at compile time. Incremental
@@ -112,7 +112,7 @@ impl GeoAwareTopologyProvider {
async fn get_topology(&self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
@@ -6,6 +6,7 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopologyError;
use std::time::Duration;
@@ -17,11 +18,7 @@ use wasmtimer::tokio::sleep;
mod accessor;
pub mod geo_aware_provider;
pub mod nym_api_provider;
pub use geo_aware_provider::GeoAwareTopologyProvider;
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_topology::provider_trait::TopologyProvider;
pub(crate) mod nym_api_provider;
// TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10;
@@ -14,10 +14,9 @@ use url::Url;
pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
#[derive(Debug)]
pub struct Config {
pub min_mixnode_performance: u8,
pub min_gateway_performance: u8,
pub(crate) struct Config {
pub(crate) min_mixnode_performance: u8,
pub(crate) min_gateway_performance: u8,
}
impl Default for Config {
@@ -30,7 +29,7 @@ impl Default for Config {
}
}
pub struct NymApiTopologyProvider {
pub(crate) struct NymApiTopologyProvider {
config: Config,
validator_client: nym_validator_client::client::NymApiClient,
@@ -41,7 +40,7 @@ pub struct NymApiTopologyProvider {
}
impl NymApiTopologyProvider {
pub fn new(
pub(crate) fn new(
config: Config,
mut nym_api_urls: Vec<Url>,
client_version: String,
@@ -99,7 +98,7 @@ impl NymApiTopologyProvider {
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
+1 -3
View File
@@ -121,9 +121,7 @@ pub async fn current_mixnodes<R: Rng>(
log::trace!("Fetching list of mixnodes from: {nym_api}");
let mixnodes = client
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let mixnodes = client.get_basic_active_mixing_assigned_nodes(None).await?;
let valid_mixnodes = mixnodes
.iter()
.filter_map(|mixnode| mixnode.try_into().ok())
@@ -19,7 +19,7 @@ use nym_api_requests::ecash::{
};
use nym_api_requests::models::{
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
@@ -30,10 +30,10 @@ use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
use nym_mixnet_contract_common::NymNodeDetails;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
@@ -106,9 +106,7 @@ impl Config {
pub struct Client<C, S = NoSigner> {
// ideally they would have been read-only, but unfortunately rust doesn't have such features
// #[deprecated(note = "please use `nym_api_client` instead")]
pub nym_api: nym_api::Client,
// pub nym_api_client: NymApiClient,
pub nyxd: NyxdClient<C, S>,
}
@@ -245,50 +243,6 @@ impl<C, S> Client<C, S> {
Ok(self.nym_api.get_gateways().await?)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -336,7 +290,7 @@ impl NymApiClient {
self.nym_api.change_base_url(new_endpoint);
}
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes instead")]
pub async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
@@ -373,7 +327,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_basic_entry_assigned_nodes(
.get_all_basic_entry_assigned_nodes(
semver_compatibility.clone(),
false,
Some(page),
@@ -394,7 +348,7 @@ impl NymApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
pub async fn get_all_basic_active_mixing_assigned_nodes(
pub async fn get_basic_active_mixing_assigned_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
@@ -424,32 +378,6 @@ impl NymApiClient {
Ok(nodes)
}
/// retrieve basic information for all bonded nodes on the network
pub async fn get_all_basic_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
loop {
let mut res = self
.nym_api
.get_basic_nodes(semver_compatibility.clone(), false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn get_cached_active_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
@@ -476,48 +404,6 @@ impl NymApiClient {
Ok(self.nym_api.get_gateways_described().await?)
}
pub async fn get_all_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn get_gateway_core_status_count(
&self,
identity: IdentityKeyRef<'_>,
@@ -11,10 +11,9 @@ use nym_api_requests::ecash::models::{
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, NymNodeDescription,
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
models::{
@@ -39,7 +38,7 @@ use nym_contracts_common::IdentityKey;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
use time::format_description::BorrowedFormatItem;
use time::Date;
use tracing::instrument;
@@ -128,44 +127,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_nodes_described(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "described"], &params)
.await
}
async fn get_nym_nodes(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], &params)
.await
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(
&self,
@@ -217,7 +178,7 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes(
async fn get_all_basic_entry_assigned_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
@@ -298,38 +259,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_basic_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[routes::API_VERSION, "unstable", "nym-nodes", "skimmed"],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
@@ -17,7 +17,6 @@ use crate::{
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
/// Full details associated with given mixnode.
@@ -648,39 +647,14 @@ impl From<LegacyMixLayer> for u8 {
export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts"
)
)]
// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
// with the addition of .cost_params_change field
#[derive(
::cosmwasm_schema::serde::Serialize,
::cosmwasm_schema::serde::Deserialize,
::std::clone::Clone,
::std::fmt::Debug,
::std::cmp::PartialEq,
::cosmwasm_schema::schemars::JsonSchema,
Default,
Copy,
)]
#[schemars(crate = "::cosmwasm_schema::schemars")]
#[cw_serde]
#[derive(Default, Copy)]
pub struct PendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
#[serde(default)]
pub cost_params_change: Option<IntervalEventId>,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct LegacyPendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
}
impl From<PendingMixNodeChanges> for LegacyPendingMixNodeChanges {
fn from(value: PendingMixNodeChanges) -> Self {
LegacyPendingMixNodeChanges {
pledge_change: value.pledge_change,
}
}
}
impl PendingMixNodeChanges {
pub fn new_empty() -> PendingMixNodeChanges {
PendingMixNodeChanges {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -112,18 +112,12 @@ impl PemStorableKeyPair for KeyPair {
}
}
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
pub struct PublicKey(x25519_dalek::PublicKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
write!(f, "{}", self.to_base58_string())
}
}
@@ -31,16 +31,8 @@ pub mod option_bs58_x25519_pubkey {
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<PublicKey>, D::Error> {
match Option::<String>::deserialize(deserializer)? {
None => Ok(None),
Some(s) => {
if s.is_empty() {
Ok(None)
} else {
Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
}
}
}
let s = Option::<String>::deserialize(deserializer)?;
s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
}
}
+3 -9
View File
@@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -119,18 +119,12 @@ impl PemStorableKeyPair for KeyPair {
}
/// ed25519 EdDSA Public Key
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct PublicKey(ed25519_dalek::VerifyingKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
write!(f, "{}", self.to_base58_string())
}
}
+12 -13
View File
@@ -35,9 +35,6 @@ pub enum HttpClientError<E: Display = String> {
source: reqwest::Error,
},
#[error("failed to deserialise received response: {source}")]
ResponseDeserialisationFailure { source: serde_json::Error },
#[error("provided url is malformed: {source}")]
MalformedUrl {
#[from]
@@ -534,17 +531,19 @@ where
}
if res.status().is_success() {
let text = res.text().await?;
match serde_json::from_str(&text) {
Ok(res) => Ok(res),
Err(source) => {
#[cfg(debug_assertions)]
{
tracing::trace!("Result:\n{:#?}", text);
}
Err(HttpClientError::ResponseDeserialisationFailure { source })
}
#[cfg(debug_assertions)]
{
let text = res.text().await.inspect_err(|err| {
tracing::error!("Couldn't even get response text: {err}");
})?;
tracing::trace!("Result:\n{:#?}", text);
serde_json::from_str(&text)
.map_err(|err| HttpClientError::GenericRequestFailure(err.to_string()))
}
#[cfg(not(debug_assertions))]
Ok(res.json().await?)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound)
} else {
-1
View File
@@ -11,7 +11,6 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum-client-ip.workspace = true
axum.workspace = true
bytes = { workspace = true }
colored.workspace = true
+3 -3
View File
@@ -1,18 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::Request;
use axum::extract::{ConnectInfo, Request};
use axum::http::header::{HOST, USER_AGENT};
use axum::http::HeaderValue;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum_client_ip::InsecureClientIp;
use colored::Colorize;
use std::net::SocketAddr;
use std::time::Instant;
use tracing::info;
pub async fn logger(
InsecureClientIp(addr): InsecureClientIp,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
request: Request,
next: Next,
) -> impl IntoResponse {
+2 -1
View File
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TestrunAssignment {
/// has nothing to do with GW identity key. This is PK from `gateways` table
pub testrun_id: i64,
pub gateway_identity_key: String,
pub gateway_pk_id: i64,
}
+34 -9
View File
@@ -42,8 +42,32 @@ impl PendingSync {
}
}
#[derive(Debug, Clone)]
pub struct BlockProcessorConfig {
pub pruning_options: PruningOptions,
pub store_precommits: bool,
}
impl Default for BlockProcessorConfig {
fn default() -> Self {
Self {
pruning_options: PruningOptions::nothing(),
store_precommits: true,
}
}
}
impl BlockProcessorConfig {
pub fn new(pruning_options: PruningOptions, store_precommits: bool) -> Self {
Self {
pruning_options,
store_precommits,
}
}
}
pub struct BlockProcessor {
pruning_options: PruningOptions,
config: BlockProcessorConfig,
cancel: CancellationToken,
synced: Arc<Notify>,
last_processed_height: u32,
@@ -65,9 +89,10 @@ pub struct BlockProcessor {
msg_modules: Vec<Box<dyn MsgModule + Send>>,
}
#[allow(clippy::too_many_arguments)]
impl BlockProcessor {
pub async fn new(
pruning_options: PruningOptions,
config: BlockProcessorConfig,
cancel: CancellationToken,
synced: Arc<Notify>,
incoming: UnboundedReceiver<BlockToProcess>,
@@ -82,7 +107,7 @@ impl BlockProcessor {
let last_pruned_height = last_pruned.try_into().unwrap_or_default();
Ok(BlockProcessor {
pruning_options,
config,
cancel,
synced,
last_processed_height,
@@ -101,7 +126,7 @@ impl BlockProcessor {
}
pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self {
self.pruning_options = pruning_options;
self.config.pruning_options = pruning_options;
self
}
@@ -128,7 +153,7 @@ impl BlockProcessor {
// we won't end up with a corrupted storage.
let mut tx = self.storage.begin_processing_tx().await?;
persist_block(&full_info, &mut tx).await?;
persist_block(&full_info, &mut tx, self.config.store_precommits).await?;
// let the modules do whatever they want
// the ones wanting the full block:
@@ -241,7 +266,7 @@ impl BlockProcessor {
#[instrument(skip(self))]
async fn prune_storage(&mut self) -> Result<(), ScraperError> {
let keep_recent = self.pruning_options.strategy_keep_recent();
let keep_recent = self.config.pruning_options.strategy_keep_recent();
let last_to_keep = self.last_processed_height - keep_recent;
info!(
@@ -282,12 +307,12 @@ impl BlockProcessor {
async fn maybe_prune_storage(&mut self) -> Result<(), ScraperError> {
debug!("checking for storage pruning");
if self.pruning_options.strategy.is_nothing() {
if self.config.pruning_options.strategy.is_nothing() {
trace!("the current pruning strategy is 'nothing'");
return Ok(());
}
let interval = self.pruning_options.strategy_interval();
let interval = self.config.pruning_options.strategy_interval();
if self.last_pruned_height + interval <= self.last_processed_height {
self.prune_storage().await?;
}
@@ -371,7 +396,7 @@ impl BlockProcessor {
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
// in case we were offline for a while,
// make sure we don't request blocks we'd have to prune anyway
let keep_recent = self.pruning_options.strategy_keep_recent();
let keep_recent = self.config.pruning_options.strategy_keep_recent();
let last_to_keep = latest_block - keep_recent;
self.last_processed_height = max(self.last_processed_height, last_to_keep);
+14 -3
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::BlockToProcess;
use crate::block_processor::BlockProcessor;
use crate::block_processor::{BlockProcessor, BlockProcessorConfig};
use crate::block_requester::{BlockRequest, BlockRequester};
use crate::error::ScraperError;
use crate::modules::{BlockModule, MsgModule, TxModule};
@@ -34,6 +34,8 @@ pub struct Config {
pub database_path: PathBuf,
pub pruning_options: PruningOptions,
pub store_precommits: bool,
}
pub struct NyxdScraperBuilder {
@@ -60,8 +62,14 @@ impl NyxdScraperBuilder {
req_rx,
processing_tx.clone(),
);
let mut block_processor = BlockProcessor::new(
let block_processor_config = BlockProcessorConfig::new(
scraper.config.pruning_options,
scraper.config.store_precommits,
);
let mut block_processor = BlockProcessor::new(
block_processor_config,
scraper.cancel_token.clone(),
scraper.startup_sync.clone(),
processing_rx,
@@ -275,8 +283,11 @@ impl NyxdScraper {
req_tx: Sender<BlockRequest>,
processing_rx: UnboundedReceiver<BlockToProcess>,
) -> Result<BlockProcessor, ScraperError> {
let block_processor_config =
BlockProcessorConfig::new(self.config.pruning_options, self.config.store_precommits);
BlockProcessor::new(
self.config.pruning_options,
block_processor_config,
self.cancel_token.clone(),
self.startup_sync.clone(),
processing_rx,
+7 -5
View File
@@ -212,6 +212,7 @@ impl ScraperStorage {
pub async fn persist_block(
block: &FullBlockInformation,
tx: &mut StorageTransaction,
store_precommits: bool,
) -> Result<(), ScraperError> {
let total_gas = crate::helpers::tx_gas_sum(&block.transactions);
@@ -224,11 +225,12 @@ pub async fn persist_block(
// persist block data
persist_block_data(&block.block, total_gas, tx).await?;
// persist commits
if let Some(commit) = &block.block.last_commit {
persist_commits(commit, &block.validators, tx).await?;
} else {
warn!("no commits for block {}", block.block.header.height)
if store_precommits {
if let Some(commit) = &block.block.last_commit {
persist_commits(commit, &block.validators, tx).await?;
} else {
warn!("no commits for block {}", block.block.header.height)
}
}
// persist txs
-4
View File
@@ -286,10 +286,6 @@ impl NymTopology {
self.get_gateway(gateway_identity).is_some()
}
pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) {
self.gateways.push(gateway)
}
pub fn set_gateways(&mut self, gateways: Vec<gateway::LegacyNode>) {
self.gateways = gateways
}
+1 -1
View File
@@ -116,7 +116,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode {
});
}
let layer = match value.role {
let layer = match value.epoch_role {
NodeRole::Mixnode { layer } => layer
.try_into()
.map_err(|_| MixnodeConversionError::InvalidLayer)?,
+1 -1
View File
@@ -68,7 +68,7 @@ pub async fn current_network_topology_async(
let api_client = NymApiClient::new(url);
let mixnodes = api_client
.get_all_basic_active_mixing_assigned_nodes(None)
.get_basic_active_mixing_assigned_nodes(None)
.await?;
let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?;
+2 -5
View File
@@ -158,13 +158,10 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
.ok_or(Error::MissingClientBandwidthEntry)?
.client_id
{
let bandwidth = storage
.get_available_bandwidth(client_id)
.await?
.ok_or(Error::MissingClientBandwidthEntry)?;
storage.create_bandwidth_entry(client_id).await?;
Ok(Some(BandwidthStorageManager::new(
storage,
ClientBandwidth::new(bandwidth.into()),
ClientBandwidth::new(Default::default()),
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
+6 -7
View File
@@ -84,13 +84,12 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
.ok_or(Error::InconsistentConsumedBytes)?
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
if spent_bandwidth > 0
&& bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
if bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
{
let success = self.remove_peer().await?;
return Ok(!success);
+15 -15
View File
@@ -433,7 +433,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -1483,9 +1483,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.89"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
@@ -1660,7 +1660,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -1702,9 +1702,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.214"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
@@ -1729,13 +1729,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.214"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -1746,7 +1746,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -1769,7 +1769,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -1928,9 +1928,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.85"
version = "2.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a"
dependencies = [
"proc-macro2",
"quote",
@@ -1954,7 +1954,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
[[package]]
@@ -2120,5 +2120,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.85",
"syn 2.0.59",
]
@@ -3689,7 +3689,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5238,7 +5239,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5573,7 +5575,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -7592,7 +7595,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -315,7 +315,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -323,7 +323,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -317,7 +317,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -319,7 +319,8 @@
"format": "uint32",
"minimum": 0.0
}
}
},
"additionalProperties": false
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -4,7 +4,6 @@
use super::helpers::must_get_gateway_bond_by_owner;
use super::storage;
use crate::constants::default_node_costs;
use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::nodes::helpers::save_new_nymnode_with_id;
use crate::nodes::transactions::add_nym_node_inner;
@@ -116,10 +115,6 @@ pub fn try_migrate_to_nymnode(
comment: "legacy gateway did not have a pre-assigned node id".to_string(),
})?;
let current_epoch =
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
let previous_epoch = current_epoch.saturating_sub(1);
// create nym-node entry
// for gateways it's quite straightforward as there are no delegations or rewards to worry about
save_new_nymnode_with_id(
@@ -130,7 +125,6 @@ pub fn try_migrate_to_nymnode(
cost_params,
info.sender.clone(),
gateway_bond.pledge_amount,
previous_epoch,
)?;
storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone());
+3 -6
View File
@@ -22,8 +22,6 @@ pub(crate) fn save_new_nymnode(
pledge: Coin,
) -> Result<NodeId, MixnetContractError> {
let node_id = next_nymnode_id_counter(storage)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
save_new_nymnode_with_id(
storage,
node_id,
@@ -32,13 +30,11 @@ pub(crate) fn save_new_nymnode(
cost_params,
owner,
pledge,
current_epoch,
)?;
Ok(node_id)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn save_new_nymnode_with_id(
storage: &mut dyn Storage,
node_id: NodeId,
@@ -47,9 +43,10 @@ pub(crate) fn save_new_nymnode_with_id(
cost_params: NodeCostParams,
owner: Addr,
pledge: Coin,
last_rewarding_epoch: u32,
) -> Result<(), MixnetContractError> {
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, last_rewarding_epoch)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height);
// save node bond data
+2 -32
View File
@@ -52,8 +52,8 @@ pub(crate) fn save_assignment(
// update metadata
let mut metadata = ROLES_METADATA.load(storage, inactive)?;
let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default();
metadata.set_highest_id(highest_id, assignment.role);
let last = assignment.nodes.last().copied().unwrap_or_default();
metadata.set_highest_id(last, assignment.role);
metadata.set_role_count(assignment.role, assignment.nodes.len() as u32);
if assignment.is_final_assignment() {
metadata.fully_assigned = true
@@ -140,7 +140,6 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
mod tests {
use super::*;
use crate::support::tests::test_helpers;
use crate::support::tests::test_helpers::TestSetup;
#[test]
fn next_id() {
@@ -150,33 +149,4 @@ mod tests {
assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap());
}
}
#[test]
fn assigning_role_uses_highest_id_even_if_not_sorted() {
let mut test = TestSetup::new();
let deps = test.deps_mut();
let sorted = RoleAssignment {
role: Role::EntryGateway,
nodes: vec![1, 2, 3],
};
let unsorted = RoleAssignment {
role: Role::Layer1,
nodes: vec![8, 5, 4],
};
save_assignment(deps.storage, sorted).unwrap();
save_assignment(deps.storage, unsorted).unwrap();
let storage = deps.as_ref().storage;
let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap();
let inactive = active_bucket.other() as u8;
let metadata = ROLES_METADATA.load(storage, inactive).unwrap();
assert_eq!(metadata.entry_gateway_metadata.highest_id, 3);
assert_eq!(metadata.layer1_metadata.highest_id, 8);
assert_eq!(metadata.highest_rewarded_id(), 8)
}
}
-9
View File
@@ -13,8 +13,6 @@ pub use nym_explorer_api_requests::{
// Paths
const API_VERSION: &str = "v1";
const TMP: &str = "tmp";
const UNSTABLE: &str = "unstable";
const MIXNODES: &str = "mix-nodes";
const GATEWAYS: &str = "gateways";
@@ -98,13 +96,6 @@ impl ExplorerClient {
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
}
pub async fn unstable_get_gateways(
&self,
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
.await
}
}
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
@@ -4,7 +4,6 @@
use crate::state::ExplorerApiStateContext;
use log::{info, warn};
use nym_explorer_api_requests::Location;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_task::TaskClient;
pub(crate) struct GeoLocateTask {
@@ -26,7 +25,6 @@ impl GeoLocateTask {
_ = interval_timer.tick() => {
self.locate_mix_nodes().await;
self.locate_gateways().await;
self.locate_nym_nodes().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
@@ -111,83 +109,6 @@ impl GeoLocateTask {
trace!("All mix nodes located");
}
async fn locate_nym_nodes(&mut self) {
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
let geo_ip = self.state.inner.geo_ip.0.clone();
for (i, cache_item) in nym_nodes.values().enumerate() {
if self
.state
.inner
.nymnodes
.is_location_valid(cache_item.node_id())
.await
{
// when the cached location is valid, don't locate and continue to next mix node
continue;
}
let bonded_host = &cache_item.bond_information.node.host;
match geo_ip.query(
bonded_host,
Some(
cache_item
.bond_information
.node
.custom_http_port
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
),
) {
Ok(opt) => match opt {
Some(location) => {
let location: Location = location.into();
trace!(
"{} mix nodes already located. host {} is located in {:#?}",
i,
bonded_host,
location.three_letter_iso_country_code,
);
if i > 0 && (i % 100) == 0 {
info!("Located {} nym-nodes...", i + 1,);
}
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), Some(location))
.await;
// one node has been located, so return out of the loop
return;
}
None => {
warn!("❌ Location for {bonded_host} not found.");
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), None)
.await;
}
},
Err(_e) => {
// warn!(
// "❌ Oh no! Location for {} failed. Error: {:#?}",
// cache_item.mix_node().host,
// e
// );
}
};
}
trace!("All nym-nodes nodes located");
}
async fn locate_gateways(&mut self) {
let gateways = self.state.inner.gateways.get_gateways().await;
-2
View File
@@ -10,7 +10,6 @@ use crate::gateways::http::gateways_make_default_routes;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes;
use crate::overview::http::overview_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::service_providers::http::service_providers_make_default_routes;
@@ -59,7 +58,6 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
};
building_rocket
-1
View File
@@ -22,7 +22,6 @@ mod http;
mod location;
mod mix_node;
pub(crate) mod mix_nodes;
mod nym_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
use okapi::openapi3::OpenApi;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
pub fn unstable_temp_nymnodes_make_default_routes(
settings: &OpenApiSettings,
) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
Json(gateways)
}
-8
View File
@@ -1,8 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::NodeId;
use crate::location::LocationCache;
pub(crate) type NymNodeLocationCache = LocationCache<NodeId>;
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
pub(crate) mod http;
pub(crate) mod location;
pub(crate) mod models;
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200);
-154
View File
@@ -1,154 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::location::{LocationCache, LocationCacheItem};
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
use nym_validator_client::models::NymNodeDescription;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{RwLock, RwLockReadGuard};
pub(crate) struct NymNodesCache {
pub(crate) valid_until: SystemTime,
pub(crate) bonded_nym_nodes: HashMap<NodeId, NymNodeDetails>,
pub(crate) described_nodes: HashMap<NodeId, NymNodeDescription>,
}
impl NymNodesCache {
fn new() -> Self {
NymNodesCache {
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
bonded_nym_nodes: Default::default(),
described_nodes: Default::default(),
}
}
// fn is_valid(&self) -> bool {
// self.valid_until >= SystemTime::now()
// }
}
#[derive(Clone)]
pub(crate) struct ThreadSafeNymNodesCache {
nymnodes: Arc<RwLock<NymNodesCache>>,
locations: Arc<RwLock<LocationCache<NodeId>>>,
}
impl ThreadSafeNymNodesCache {
pub(crate) fn new() -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(NymNodeLocationCache::new())),
}
}
pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(locations)),
}
}
pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool {
self.locations
.read()
.await
.get(&node_id)
.map_or(false, |cache_item| {
cache_item.valid_until > SystemTime::now()
})
}
pub(crate) async fn get_bonded_nymnodes(
&self,
) -> RwLockReadGuard<HashMap<NodeId, NymNodeDetails>> {
let guard = self.nymnodes.read().await;
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
}
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
self.locations.read().await.clone()
}
pub(crate) async fn set_location(&self, node_id: NodeId, location: Option<Location>) {
// cache the location for this mix node so that it can be used when the mix node list is refreshed
self.locations
.write()
.await
.insert(node_id, LocationCacheItem::new_from_location(location));
}
pub(crate) async fn update_cache(
&self,
all_bonds: Vec<NymNodeDetails>,
descriptions: Vec<NymNodeDescription>,
) {
let mut guard = self.nymnodes.write().await;
guard.bonded_nym_nodes = all_bonds
.into_iter()
.map(|details| (details.node_id(), details))
.collect();
guard.described_nodes = descriptions
.into_iter()
.map(|description| (description.node_id, description))
.collect();
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
}
pub(crate) async fn pretty_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
let nodes_guard = self.nymnodes.read().await;
let location_guard = self.locations.read().await;
let mut pretty_gateways = vec![];
for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes {
let Some(description) = nodes_guard.described_nodes.get(node_id) else {
continue;
};
if description.description.declared_role.entry {
let location = location_guard.get(node_id);
let bond = &native_nymnode.bond_information;
pretty_gateways.push(PrettyDetailedGatewayBond {
pledge_amount: bond.original_pledge.clone(),
owner: bond.owner.clone(),
block_height: bond.bonding_height,
gateway: Gateway {
host: bond.node.host.clone(),
mix_port: description.description.mix_port(),
clients_port: description.description.mixnet_websockets.ws_port,
location: description
.description
.auxiliary_details
.location
.as_ref()
.map(|l| l.to_string())
.unwrap_or_default(),
sphinx_key: description
.description
.host_information
.keys
.x25519
.to_base58_string(),
identity_key: bond.node.identity_key.clone(),
version: description
.description
.build_information
.build_version
.clone(),
},
proxy: None,
location: location.and_then(|l| l.location.clone()),
})
}
}
pretty_gateways
}
}
-9
View File
@@ -18,8 +18,6 @@ use crate::gateways::models::ThreadsafeGatewayCache;
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::location::MixnodeLocationCache;
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
use crate::ping::models::ThreadsafePingCache;
use crate::validators::models::ThreadsafeValidatorCache;
@@ -32,7 +30,6 @@ pub struct ExplorerApiState {
pub(crate) gateways: ThreadsafeGatewayCache,
pub(crate) mixnode: ThreadsafeMixNodeCache,
pub(crate) mixnodes: ThreadsafeMixNodesCache,
pub(crate) nymnodes: ThreadSafeNymNodesCache,
pub(crate) ping: ThreadsafePingCache,
pub(crate) validators: ThreadsafeValidatorCache,
pub(crate) geo_ip: ThreadsafeGeoIp,
@@ -52,7 +49,6 @@ pub struct ExplorerApiStateOnDisk {
pub(crate) country_node_distribution: CountryNodesDistribution,
pub(crate) mixnode_location_cache: MixnodeLocationCache,
pub(crate) gateway_location_cache: GatewayLocationCache,
pub(crate) nymnode_location_cache: NymNodeLocationCache,
pub(crate) as_at: DateTime<Utc>,
}
@@ -89,9 +85,6 @@ impl ExplorerApiStateContext {
mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(
state.mixnode_location_cache,
),
nymnodes: ThreadSafeNymNodesCache::new_with_location_cache(
state.nymnode_location_cache,
),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -108,7 +101,6 @@ impl ExplorerApiStateContext {
gateways: ThreadsafeGatewayCache::new(),
mixnode: ThreadsafeMixNodeCache::new(),
mixnodes: ThreadsafeMixNodesCache::new(),
nymnodes: ThreadSafeNymNodesCache::new(),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -125,7 +117,6 @@ impl ExplorerApiStateContext {
country_node_distribution: self.inner.country_node_distribution.get_all().await,
mixnode_location_cache: self.inner.mixnodes.get_locations().await,
gateway_location_cache: self.inner.gateways.get_locations().await,
nymnode_location_cache: self.inner.nymnodes.get_locations().await,
as_at: Utc::now(),
};
serde_json::to_writer(file, &state).expect("error writing state to disk");
+5 -54
View File
@@ -1,16 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
use nym_mixnet_contract_common::GatewayBond;
use nym_task::TaskClient;
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
use nym_validator_client::models::MixNodeBondAnnotated;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
use std::future::Future;
use tokio::time::MissedTickBehavior;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
pub(crate) struct ExplorerApiTasks {
state: ExplorerApiStateContext,
@@ -39,28 +39,6 @@ impl ExplorerApiTasks {
bonds
}
async fn retrieve_bonded_nymnodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
info!("About to retrieve all nymnode bonds...");
self.state
.inner
.validator_client
.0
.get_all_cached_bonded_nym_nodes()
.await
}
async fn retrieve_node_descriptions(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
info!("About to retrieve node descriptions...");
self.state
.inner
.validator_client
.0
.get_all_cached_described_nodes()
.await
}
async fn retrieve_all_mixnodes(&self) -> Vec<MixNodeBondAnnotated> {
info!("About to retrieve all mixnode bonds...");
self.retrieve_mixnodes(
@@ -152,33 +130,10 @@ impl ExplorerApiTasks {
}
}
async fn update_nymnodes_cache(&self) {
let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| {
error!("failed to retrieve nym node bonds: {err}");
Vec::new()
});
let all_descriptions = self
.retrieve_node_descriptions()
.await
.unwrap_or_else(|err| {
error!("failed to retrieve node descriptions: {err}");
Vec::new()
});
self.state
.inner
.nymnodes
.update_cache(nym_node_bonds, all_descriptions)
.await
}
pub(crate) fn start(mut self) {
info!("Spawning mix nodes task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
@@ -192,10 +147,6 @@ impl ExplorerApiTasks {
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating nymnode cache...");
self.update_nymnodes_cache().await;
info!("Done");
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
-40
View File
@@ -1,40 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-50
View File
@@ -1,50 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
Starting Docker:
```bash
docker-compose pull
docker-compose up -d
```
Stopping Docker:
```bash
docker-compose down
```
First, run the development server:
```bash
npm run dev -- -p 8080
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:8080/](http://localhost:8080/) 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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## 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/app/building-your-application/deploying) for more details.
@@ -1,43 +0,0 @@
version: "2"
services:
remark:
# remove the next line in case you want to use this Docker Compose file separately
# as otherwise it would complain for absence of Dockerfile
build: .
image: umputun/remark42:latest
container_name: "remark42"
hostname: "remark42"
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# uncomment to expose directly (no proxy)
ports:
- "8081:8080"
- "443:8443"
environment:
- REMARK_URL=http://localhost:8081
- SECRET=secret-key
- AUTH_ANON=true
- SITE=remark42
# - DEBUG=true
# - AUTH_GOOGLE_CID
# - AUTH_GOOGLE_CSEC
# - AUTH_GITHUB_CID
# - AUTH_GITHUB_CSEC
# - AUTH_FACEBOOK_CID
# - AUTH_FACEBOOK_CSEC
# - AUTH_DISQUS_CID
# - AUTH_DISQUS_CSEC
# Enable it only for the initial comment import or for manual backups.
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
# to keep creating backups manually!
- ADMIN_PASSWD=password
volumes:
- ./var:/srv/var
-7
View File
@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
-24
View File
@@ -1,24 +0,0 @@
{
"name": "remark42-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028",
"next": "15.0.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.2"
}
}
@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
@@ -1,42 +0,0 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
@@ -1,33 +0,0 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}
@@ -1,168 +0,0 @@
.page {
--gray-rgb: 0, 0, 0;
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
display: grid;
grid-template-rows: 20px 1fr 20px;
align-items: center;
justify-items: center;
min-height: 100svh;
padding: 80px;
gap: 64px;
font-family: var(--font-geist-sans);
}
@media (prefers-color-scheme: dark) {
.page {
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
}
}
.main {
display: flex;
flex-direction: column;
gap: 32px;
grid-row-start: 2;
}
.main ol {
font-family: var(--font-geist-mono);
padding-left: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: -0.01em;
list-style-position: inside;
}
.main li:not(:last-of-type) {
margin-bottom: 8px;
}
.main code {
font-family: inherit;
background: var(--gray-alpha-100);
padding: 2px 4px;
border-radius: 4px;
font-weight: 600;
}
.ctas {
display: flex;
gap: 16px;
}
.ctas a {
appearance: none;
border-radius: 128px;
height: 48px;
padding: 0 20px;
border: none;
border: 1px solid transparent;
transition:
background 0.2s,
color 0.2s,
border-color 0.2s;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 20px;
font-weight: 500;
}
a.primary {
background: var(--foreground);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--gray-alpha-200);
min-width: 180px;
}
.footer {
grid-row-start: 3;
display: flex;
gap: 24px;
}
.footer a {
display: flex;
align-items: center;
gap: 8px;
}
.footer img {
flex-shrink: 0;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
.footer a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
}
@media (max-width: 600px) {
.page {
padding: 32px;
padding-bottom: 80px;
}
.main {
align-items: center;
}
.main ol {
text-align: center;
}
.ctas {
flex-direction: column;
}
.ctas a {
font-size: 14px;
height: 40px;
padding: 0 16px;
}
a.secondary {
min-width: auto;
}
.footer {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
}
@@ -1,57 +0,0 @@
import Head from "next/head";
import Script from "next/script";
import { Box } from "@mui/material";
export default function Home() {
return (
<div>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Box
display={"flex"}
flexDirection={"column"}
maxWidth={"60%"}
margin={"50px auto"}
>
<div>NymNode X</div>
<div>Info about NymNode X</div>
<div id="remark42"></div>
{/* Configuration Script */}
<Script
id="remark-config"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
var remark_config = {
host: 'http://localhost:8081', // Updated to match the REMARK_URL
site_id: 'remark42',
components: ['embed', 'last-comments'],
max_shown_comments: 100,
theme: 'light',
page_title: 'My custom title for a page',
locale: 'en',
show_email_subscription: false,
simple_view: true,
no_footer: true
};
`,
}}
/>
{/* Initialization Script */}
<Script
id="remark-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);
`,
}}
/>
</Box>
</div>
);
}
-27
View File
@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.
Binary file not shown.
-190
View File
@@ -1,190 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import {
EXPLORER_API,
COSMOS_API,
VALIDATOR_API_EPOCH,
VALIDATOR_API_SUPPLY,
HARBOURMASTER_API_SUMMARY,
HARBOURMASTER_API_MIXNODES_STATS,
HARBOURMASTER_API_BASE,
CURRENT_EPOCH,
CURRENT_EPOCH_REWARDS,
CIRCULATING_NYM_SUPPLY,
} from "../urls";
export interface ExplorerData {
circulatingNymSupplyData: any;
nymNodesData: any;
packetsAndStakingData: any;
currentEpochData: any;
currentEpochRewardsData: any;
}
export interface ExplorerCache {
data?: ExplorerData;
lastUpdated?: Date;
}
declare global {
// Extend the global object with our custom property
var explorerCache: ExplorerCache | undefined;
}
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
const getExplorerData = async () => {
// FETCH NYMNODES
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
// refresh event list cache at given interval
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
});
// FETCH CURRENT EPOCH
const fetchCurrentEpoch = await fetch(CURRENT_EPOCH, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
// refresh event list cache at given interval
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
});
// FETCH CURRENT EPOCH REWARDS
const fetchCurrentEpochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
// refresh event list cache at given interval
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
});
// FETCH CIRCULATING NYM SUPPLY
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
// refresh event list cache at given interval
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
});
// FETCH PACKETS AND STAKING
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
// refresh event list cache at given interval
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
});
const [
circulatingNymSupplyRes,
nymNodesRes,
packetsAndStakingRes,
currentEpochRes,
currentEpochRewardsRes,
] = await Promise.all([
fetchCirculatingNymSupply,
fetchNymNodes,
fetchPacketsAndStaking,
fetchCurrentEpoch,
fetchCurrentEpochRewards,
]);
const [
circulatingNymSupplyData,
nymNodesData,
packetsAndStakingData,
currentEpochData,
currentEpochRewardsData,
] = await Promise.all([
circulatingNymSupplyRes.json(),
nymNodesRes.json(),
packetsAndStakingRes.json(),
currentEpochRes.json(),
currentEpochRewardsRes.json(),
]);
return [
circulatingNymSupplyData,
nymNodesData,
packetsAndStakingData,
currentEpochData,
currentEpochRewardsData,
];
};
export async function ensureCacheExists() {
// makes sure the cache exists in global memory
let doUpdate = false;
const now = new Date();
if (!global.explorerCache) {
global.explorerCache = {};
doUpdate = true;
}
if (
global.explorerCache.lastUpdated &&
now.getDate() - global.explorerCache.lastUpdated.getDate() >
CACHE_TIME_SECONDS
) {
doUpdate = true;
}
// if the cache has expired or never existed, get it from API's
if (doUpdate) {
const [
circulatingNymSupplyData,
nymNodesData,
packetsAndStakingData,
currentEpochData,
currentEpochRewardsData,
] = await getExplorerData();
packetsAndStakingData.pop();
global.explorerCache.data = {
circulatingNymSupplyData,
nymNodesData,
packetsAndStakingData,
currentEpochData,
currentEpochRewardsData,
};
global.explorerCache.lastUpdated = now;
}
}
export async function getCacheExplorerData() {
await ensureCacheExists();
if (!global.explorerCache?.data) {
return null;
}
return global.explorerCache.data || null;
}
/**
* This is a custom API route that returns metadata from Strapi about images: height, width, strapi download url.
*
* The response from Strapi is cached in memory for CACHE_TIME_SECONDS.
*/
// export default async function handler(
// req: NextApiRequest,
// res: NextApiResponse
// ) {
// // return cached data
// const data = await getCacheExplorerData();
// if (data) {
// res.status(200).json(data);
// res.end();
// }
// // catch-all
// res.status(404).end();
// }
-18
View File
@@ -1,18 +0,0 @@
export const HARBOURMASTER_API_SUMMARY =
"https://harbourmaster.nymtech.net/v2/summary";
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
export const VALIDATOR_API_SUPPLY =
"https://validator.nymtech.net/api/v1/circulating-supply";
export const COSMOS_API =
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
export const VALIDATOR_API_EPOCH =
"https://validator.nymtech.net/api/v1/epoch/reward_params";
export const HARBOURMASTER_API_MIXNODES_STATS =
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
export const CURRENT_EPOCH =
" https://validator.nymtech.net/api/v1/epoch/current";
export const CURRENT_EPOCH_REWARDS =
"https://validator.nymtech.net/api/v1/epoch/reward_params";
export const CIRCULATING_NYM_SUPPLY =
"https://validator.nymtech.net/api/v1/circulating-supply";
@@ -1,307 +0,0 @@
import * as React from "react";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
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 Typography from "@mui/material/Typography";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { Card, CardContent } from "@mui/material";
import { ExplorerStaticProgressBar } from "./ExplorerStaticProgressBar";
import { MultiSegmentProgressBar } from "./ExplorerMultiSegmentProgressBar";
import useMediaQuery from "@mui/material/useMediaQuery";
import CircleIcon from "@mui/icons-material/Circle";
export interface IAccontStatsRowProps {
type: string;
allocation: number;
amount: number;
value: number;
history?: { type: string; amount: number }[];
isLastRow?: boolean;
progressBarColor?: string;
}
const progressBarColours = [
"#BEF885",
"#7FB0FF",
"#00D17D",
"#004650",
"#FEECB3",
];
const TABLET_WIDTH = "(min-width:700px)";
const Row = (props: IAccontStatsRowProps) => {
const tablet = useMediaQuery(TABLET_WIDTH);
const {
type,
allocation,
amount,
value,
history,
isLastRow,
progressBarColor,
} = props;
const [open, setOpen] = React.useState(false);
return (
<React.Fragment>
{/* Main Row */}
{tablet ? (
<TableRow>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "25%",
}}
>
<Typography>{type}</Typography>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "25%",
}}
>
<Box>
<Typography>{allocation}%</Typography>
<ExplorerStaticProgressBar
value={allocation}
color={progressBarColor || "green"}
/>
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
{amount} NYM
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
$ {value}
</TableCell>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "10%",
}}
>
{history && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
</TableRow>
) : (
// MOBILE VIEW
<TableRow>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "45%",
}}
>
<Box display={"flex"} gap={1} alignItems={"center"}>
<CircleIcon sx={{ color: progressBarColor }} fontSize="small" />
{type}
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "45%",
}}
>
<Typography>{amount} NYM</Typography>
<Typography>$ {value}</Typography>
</TableCell>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "10%",
}}
>
{history && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
</TableRow>
)}
{/* History Rows */}
{history &&
open &&
history.map((historyRow, i) => (
<TableRow key={i}>
<TableCell
sx={{
display: "flex",
alignItems: "center",
pl: 5,
borderBottom: "none", // Explicitly remove border
}}
>
<span style={{ marginRight: 8 }}></span>
{historyRow.type}
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: "none", // Explicitly remove border
}}
>
{historyRow.amount}
</TableCell>
<TableCell
sx={{
borderBottom: "none", // Explicitly remove border
}}
>
{/* Any additional content */}
</TableCell>
</TableRow>
))}
</React.Fragment>
);
};
export interface IAccountStatsCardProps {
rows: Array<IAccontStatsRowProps>;
overTitle?: string;
priceTitle?: number;
}
export const AccountStatsCard = (props: IAccountStatsCardProps) => {
const { rows, overTitle, priceTitle } = props;
const tablet = useMediaQuery(TABLET_WIDTH);
const progressBarPercentages = () => {
return rows.map((row, i) => row.allocation);
};
const getProgressValues = () => {
const percentages = progressBarPercentages();
const result: Array<{ percentage: number; color: string }> = [];
percentages.map((value, i) => {
result.push({
percentage: value,
color: progressBarColours[i],
});
});
return result;
};
const progressValues = getProgressValues();
return (
<Card sx={{ height: "100%", borderRadius: "unset" }}>
<CardContent>
{overTitle && (
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
{overTitle}
</Typography>
)}
{priceTitle && (
<Typography fontSize={24} mb={3}>
${priceTitle}
</Typography>
)}
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
<TableContainer>
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
<TableHead>
{tablet ? (
<TableRow>
<TableCell>
<Typography textTransform={"uppercase"}>Type</Typography>
</TableCell>
<TableCell align="right">
<Typography textTransform={"uppercase"}>
Allocation
</Typography>
</TableCell>
<TableCell align="right">
<Typography textTransform={"uppercase"}>Amount</Typography>
</TableCell>
<TableCell align="right">
<Typography textTransform={"uppercase"}>Value</Typography>
</TableCell>
<TableCell></TableCell>
</TableRow>
) : (
<TableRow>
<TableCell>
<Typography textTransform={"uppercase"}>Type</Typography>
</TableCell>
<TableCell align="right">
<Typography textTransform={"uppercase"}>
Amount / Value
</Typography>
</TableCell>
<TableCell></TableCell>
</TableRow>
)}
</TableHead>
<TableBody>
{rows.map((row, i) => (
<Row
key={i}
{...row}
isLastRow={i === rows.length - 1}
progressBarColor={progressBarColours[i]}
/>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
);
};
@@ -1,391 +0,0 @@
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
import React, { FC, ReactElement, ReactEventHandler, useEffect } from "react";
import { ExplorerLineChart, IExplorerLineChartData } from "./ExplorerLineChart";
import {
ExplorerDynamicProgressBar,
IExplorerDynamicProgressBarProps,
} from "./ExplorerDynamicProgressBar";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import { NymTokenSVG } from "../icons/NymTokenSVG";
import { CopyToClipboard } from "@nymproject/react/clipboard/CopyToClipboard";
import Image from "next/image";
import profileImagePlaceholder from "../../public/profileImagePlaceholder.png";
import Flag from "react-world-flags";
import { QRCodeCanvas } from "qrcode.react";
import StarIcon from "@mui/icons-material/Star";
import Script from "next/script";
import { useMainContext } from "../context/main";
declare global {
interface Window {
remark_config: {
host: string;
site_id: string;
components: string[];
max_shown_comments: number;
theme: string;
locale: string;
show_email_subscription: boolean;
simple_view: boolean;
no_footer: boolean;
};
REMARK42: {
createInstance: (config: typeof window.remark_config) => void;
changeTheme: (theme: "light" | "dark") => void;
};
}
}
interface ICardUpDownPriceLineProps {
percentage: number;
numberWentUp: boolean;
}
const CardUpDownPriceLine = (
props: ICardUpDownPriceLineProps
): ReactElement => {
const { percentage, numberWentUp } = props;
return (
<Box mb={3} display={"flex"}>
{numberWentUp ? (
<ArrowUpwardIcon sx={{ color: "#00CA33" }} fontSize="small" />
) : (
<ArrowDownwardIcon sx={{ color: "#DF1400" }} fontSize="small" />
)}
<Typography sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}>
{percentage}% (24H)
</Typography>
</Box>
);
};
interface ICardTitlePriceProps {
price: number;
upDownLine: ICardUpDownPriceLineProps;
}
const CardTitlePrice = (props: ICardTitlePriceProps): React.ReactNode => {
const { price, upDownLine } = props;
return (
<Box display={"flex"} flexDirection={"column"} alignItems={"flex-end"}>
<Box display={"flex"} justifyContent={"space-between"} width={"100%"}>
<Box display={"flex"} gap={1}>
<NymTokenSVG />
<Typography>NYM</Typography>
</Box>
<Typography>${price}</Typography>
</Box>
<CardUpDownPriceLine {...upDownLine} />
</Box>
);
};
export interface ICardDataRowsProps {
rows: Array<{ key: string; value: string }>;
}
export const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
const { rows } = props;
return (
<Box mb={3}>
{rows.map((row, i) => {
return (
<Box
key={i}
paddingTop={2}
paddingBottom={2}
display={"flex"}
justifyContent={"space-between"}
borderBottom={i === 0 ? "1px solid #C3D7D7" : "none"}
>
<Typography>{row.key}</Typography>
<Typography>{row.value}</Typography>
</Box>
);
})}
</Box>
);
};
interface ICardProileImage {
url?: string;
}
const CardProfileImage = (props: ICardProileImage) => {
const { url } = props;
return (
<Box display={"flex"} justifyContent={"flex-start"} mb={3}>
{url ? (
<Image src={url} alt="linkedIn" width={80} height={80} />
) : (
<Image
src={profileImagePlaceholder}
alt="linkedIn"
width={80}
height={80}
/>
)}
</Box>
);
};
interface ICardProfileCountry {
countryCode: string;
countryName: string;
}
const CardProfileCountry = (props: ICardProfileCountry) => {
const { countryCode, countryName } = props;
return (
<Box display={"flex"} justifyContent={"flex-start"} gap={2} mb={3}>
<Flag code={countryCode} width="20" />
<Typography textTransform={"uppercase"}>{countryName}</Typography>
</Box>
);
};
interface ICardCopyAddressProps {
title: string;
address: string;
}
const CardCopyAddress = (props: ICardCopyAddressProps) => {
const { title, address } = props;
return (
<Box
paddingTop={2}
paddingBottom={2}
display={"flex"}
flexDirection={"column"}
gap={2}
borderBottom={"1px solid #C3D7D7"}
>
<Typography textTransform={"uppercase"}>{title}</Typography>
<Box display={"flex"} justifyContent={"space-between"}>
<Typography>{address}</Typography>
<CopyToClipboard
sx={{ mr: 0.5, color: "grey.400" }}
smallIcons
value={address}
tooltip={`Copy identity key ${address} to clipboard`}
/>
</Box>
</Box>
);
};
interface ICardQRCodeProps {
url: string;
}
const CardQRCode = (props: ICardQRCodeProps) => {
const { url } = props;
return (
<Box display={"flex"} justifyContent={"flex-start"}>
<Box
padding={2}
border={"1px solid #C3D7D7"}
mb={3}
display={"block"}
width={"unset"}
>
<QRCodeCanvas value={url} />
</Box>
</Box>
);
};
interface ICardRatingsProps {
ratings: Array<{ title: string; numberOfStars: number }>;
}
const CardRatings = (props: ICardRatingsProps) => {
const { ratings } = props;
return (
<Box mb={3}>
{ratings.map((rating, i) => {
const Stars = () => {
const stars = [];
for (let i = 0; i < rating.numberOfStars; i++) {
stars.push(<StarIcon sx={{ color: "#14E76F" }} fontSize="small" />);
}
return stars;
};
const RatingTitle = () => {
if (rating.numberOfStars === 1) {
return <Typography>Bad</Typography>;
} else if (rating.numberOfStars === 2) {
return <Typography>Bad</Typography>;
} else if (rating.numberOfStars === 3) {
return <Typography>ok</Typography>;
} else if (rating.numberOfStars === 4) {
return <Typography>Good</Typography>;
} else {
return <Typography>Excellent</Typography>;
}
};
return (
<Box
key={i}
paddingTop={2}
paddingBottom={2}
display={"flex"}
justifyContent={"space-between"}
borderBottom={i < ratings.length - 1 ? "1px solid #C3D7D7" : "none"}
>
<Typography>{rating.title}</Typography>
<Box display={"flex"} gap={1} alignItems={"center"}>
<Stars />
<RatingTitle />
</Box>
</Box>
);
})}
</Box>
);
};
const CardChat = () => {
const { mode } = useMainContext();
useEffect(() => {
if (typeof window !== "undefined") {
// Set Remark42 configuration on the window object
window.remark_config = {
host: "https://remark.blockfend.com",
site_id: "nym-explorer-test",
components: ["embed", "last-comments"],
max_shown_comments: 100,
theme: mode === "light" ? "light" : "dark",
locale: "en",
show_email_subscription: false,
simple_view: true,
no_footer: true,
};
// Dynamically load the Remark42 script if it doesn't exist
if (!document.getElementById("remark42-script")) {
const script = document.createElement("script");
script.src = `${window.remark_config.host}/web/embed.js`;
script.async = true;
script.defer = true;
script.id = "remark42-script";
document.body.appendChild(script);
} else if (window.REMARK42) {
// Re-initialize if the script is already loaded
window.REMARK42.createInstance(window.remark_config);
}
}
}, []);
// React to mode changes and update Remark42 theme
useEffect(() => {
if (window.REMARK42 && window.REMARK42.changeTheme) {
window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
}
}, [mode]);
return (
<Box>
<div id="remark42" className="remark"></div>
<Script
id="remark-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
if (window.REMARK42) {
window.REMARK42.createInstance(window.remark_config);
}
`,
}}
/>
</Box>
);
};
export type ContentCardProps = {
overTitle?: string;
profileImage?: ICardProileImage;
title?: string | number;
profileCountry?: ICardProfileCountry;
upDownLine?: ICardUpDownPriceLineProps;
titlePrice?: ICardTitlePriceProps;
dataRows?: ICardDataRowsProps;
graph?: { data: Array<IExplorerLineChartData>; color: string; label: string };
progressBar?: IExplorerDynamicProgressBarProps;
paragraph?: string;
onClick?: ReactEventHandler;
nymAddress?: ICardCopyAddressProps;
identityKey?: ICardCopyAddressProps;
qrCode?: ICardQRCodeProps;
ratings?: ICardRatingsProps;
chat?: boolean;
button?: {
onClick: () => void;
label: string;
};
};
export const ExplorerCard: FC<ContentCardProps> = ({
title,
titlePrice,
overTitle,
upDownLine,
dataRows,
graph,
progressBar,
paragraph,
onClick,
profileImage,
profileCountry,
nymAddress,
identityKey,
qrCode,
ratings,
chat,
button,
}) => (
<Card onClick={onClick} sx={{ height: "100%", borderRadius: "unset" }}>
<CardContent>
{overTitle && (
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
{overTitle}
</Typography>
)}
{profileImage && <CardProfileImage {...profileImage} />}
{title && (
<Typography fontSize={24} mb={3}>
{title}
</Typography>
)}
{profileCountry && <CardProfileCountry {...profileCountry} />}
{upDownLine && <CardUpDownPriceLine {...upDownLine} />}
{titlePrice && <CardTitlePrice {...titlePrice} />}
{qrCode && <CardQRCode {...qrCode} />}
{nymAddress && <CardCopyAddress {...nymAddress} />}
{identityKey && <CardCopyAddress {...identityKey} />}
{dataRows && <CardDataRows {...dataRows} />}
{ratings && <CardRatings {...ratings} />}
{graph && (
<Box mb={3}>
<ExplorerLineChart
data={graph.data}
color={graph.color}
label={graph.label}
/>
</Box>
)}
{progressBar && (
<Box mb={3}>
<ExplorerDynamicProgressBar {...progressBar} />
</Box>
)}
{paragraph && <Typography>{paragraph}</Typography>}
{chat && <CardChat />}
{button && (
<Button onClick={button.onClick} variant="contained">
{button.label}
</Button>
)}
</CardContent>
</Card>
);
@@ -1,101 +0,0 @@
import * as React from "react";
import Box from "@mui/material/Box";
import LinearProgress from "@mui/material/LinearProgress";
import { Typography } from "@mui/material";
export interface IExplorerDynamicProgressBarProps {
title?: string;
start: string; // Start timestamp as ISO 8601 string
showEpoch: boolean;
}
export const ExplorerDynamicProgressBar = (
props: IExplorerDynamicProgressBarProps
) => {
const { start, showEpoch, title } = props;
const [progress, setProgress] = React.useState(0);
React.useEffect(() => {
// Parse the start timestamp
const startTime = new Date(start).getTime();
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
// Validate start timestamp
if (isNaN(startTime)) {
console.error("Invalid start timestamp:", { start });
return;
}
// Function to calculate progress
const calculateProgress = () => {
const currentTime = Date.now();
if (currentTime < startTime) {
return 0;
}
if (currentTime >= endTime) {
return 100;
}
const elapsed = currentTime - startTime;
const total = endTime - startTime;
return (elapsed / total) * 100;
};
// Set initial progress and start timer
setProgress(calculateProgress());
const timer = setInterval(() => {
setProgress(calculateProgress());
}, 60000); // Update every minute (60000 milliseconds)
// Cleanup on unmount
return () => {
clearInterval(timer);
};
}, [start]);
// Helper function to format date
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based
const year = date.getFullYear();
return `${hours}:${minutes}, ${day}/${month}/${year}`;
};
const startTime = new Date(start).getTime();
const endTime = startTime + 60 * 60 * 1000;
return (
<Box sx={{ width: "100%" }}>
{title && (
<Typography mb={2} textTransform={"uppercase"}>
{title}
</Typography>
)}
<LinearProgress
variant="determinate"
value={progress}
sx={{
backgroundColor: "#CAD6D7",
"& .MuiLinearProgress-bar": {
backgroundColor: "#14E76F",
},
}}
/>
{showEpoch && (
<Box mt={2}>
<Box display={"flex"} justifyContent={"space-between"}>
<Typography textTransform={"uppercase"}>START:</Typography>
<Typography> {startTime ? formatDate(startTime) : ""}</Typography>
</Box>
<Box display={"flex"} justifyContent={"space-between"}>
<Typography textTransform={"uppercase"}>END:</Typography>
<Typography> {endTime ? formatDate(endTime) : ""}</Typography>
</Box>
</Box>
)}
</Box>
);
};
@@ -1,153 +0,0 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import dynamic from "next/dynamic";
import Loading from "./Loading";
import { useEffect, useState } from "react";
const LineChart = dynamic(
() => import("@nivo/line").then((m) => m.ResponsiveLine),
{
loading: () => <Loading />,
ssr: false,
}
);
export interface IExplorerLineChartData {
date_utc: string;
numericData?: number;
// purpleLineNumericData?: number;
}
interface IAxes {
x: Date;
y: number;
}
interface ILineAxes {
id: string;
data: Array<IAxes>;
}
export const ExplorerLineChart = ({
data,
color,
label,
}: {
data: Array<IExplorerLineChartData>;
color: string;
label: string;
}) => {
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up("lg"));
const [chartData, setChartData] = useState<Array<ILineAxes>>();
useEffect(() => {
const resultData = transformData(data);
if (resultData.length > 0) {
setChartData(resultData);
}
}, [data]);
const transformData = (data: Array<IExplorerLineChartData>) => {
const lineData: ILineAxes = {
id: label,
data: [],
};
// const purpleLineData: ILineAxes = {
// id: "Numeric Data 2",
// data: [],
// };
data.map((item: any) => {
const axesGreenLineData: IAxes = {
x: new Date(item.date_utc),
y: item.numericData,
};
lineData.data.push(axesGreenLineData);
// const axesPurpleLineData: IAxes = {
// x: new Date(item.date_utc),
// y: item.purpleLineNumericData,
// };
// purpleLineData.data.push(axesPurpleLineData);
});
return [{ ...lineData }];
};
const yformat = (num: number | string | Date) => {
if (typeof num === "number") {
if (num >= 1000000000) {
return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B";
}
if (num >= 1000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
}
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
}
return num;
} else {
throw new Error("Unexpected value");
}
};
return (
<Box width={"100%"} height={isDesktop ? 200 : 150}>
{chartData && (
<LineChart
curve="monotoneX"
colors={[color]}
data={chartData}
animate
enableSlices="x"
margin={{
bottom: 24,
left: 36,
right: 16,
top: 20,
}}
theme={{
grid: { line: { strokeWidth: 0 } },
tooltip: { container: { color: "black" } },
axis: {
domain: {
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
},
ticks: {
text: {
fill: "#818386",
},
},
legend: {
text: {
fill: "#818386",
},
},
},
}}
xScale={{
type: "time",
format: "%Y-%m-%d",
}}
yScale={{ min: 1, type: "linear" }}
xFormat="time:%Y-%m-%d"
axisLeft={{
legendOffset: 12,
tickSize: 3,
format: yformat,
tickValues: 5,
}}
axisBottom={{
format: "%b %d",
legendOffset: -12,
tickValues:
chartData[0].data.length > 7 ? "every 4 days" : "every day",
}}
/>
)}
</Box>
);
};
@@ -1,36 +0,0 @@
import React from "react";
import { Box } from "@mui/material";
export interface MultiSegmentProgressBarProps {
values: { percentage: number; color: string }[]; // Array of percentage and color pairs
height?: number; // Optional height, default is 8
borderRadius?: number; // Optional border radius, default is 4
backgroundColor?: string; // Optional background color for the bar, default is light gray
}
export const MultiSegmentProgressBar: React.FC<
MultiSegmentProgressBarProps
> = ({ values, height = 8, borderRadius = 4, backgroundColor = "#CAD6D7" }) => {
return (
<Box
sx={{
display: "flex",
width: "100%",
height,
borderRadius,
overflow: "hidden",
backgroundColor,
}}
>
{values.map((value, index) => (
<Box
key={index}
sx={{
width: `${value.percentage}%`,
backgroundColor: value.color,
}}
/>
))}
</Box>
);
};
@@ -1,31 +0,0 @@
import * as React from "react";
import Box from "@mui/material/Box";
import LinearProgress from "@mui/material/LinearProgress";
export interface IExplorerStaticProgressBarProps {
color: string;
value: number;
}
export const ExplorerStaticProgressBar = (
props: IExplorerStaticProgressBarProps
) => {
const { color, value } = props;
return (
<Box>
<LinearProgress
variant="determinate"
value={value}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: "#CAD6D7",
"& .MuiLinearProgress-bar": {
backgroundColor: color,
},
}}
/>
</Box>
);
};
@@ -1,65 +0,0 @@
import React, { useState } from "react";
import { Box, Button } from "@mui/material";
interface TwoSidedSwitchProps {
leftLabel: string; // Label for the left side
rightLabel: string; // Label for the right side
onSwitch?: (side: "left" | "right") => void; // Callback when switched
}
const TwoSidedSwitch: React.FC<TwoSidedSwitchProps> = ({
leftLabel,
rightLabel,
onSwitch,
}) => {
const [selectedSide, setSelectedSide] = useState<"left" | "right">("left");
const handleSwitch = (side: "left" | "right") => {
setSelectedSide(side);
if (onSwitch) onSwitch(side);
};
return (
<Box
sx={{
display: "flex",
borderRadius: "8px",
overflow: "hidden",
width: "200px",
height: "40px",
border: "2px solid black",
}}
>
<Button
onClick={() => handleSwitch("left")}
sx={{
flex: 1,
backgroundColor: selectedSide === "left" ? "black" : "white",
color: selectedSide === "left" ? "white" : "black",
borderRadius: 0,
"&:hover": {
backgroundColor: selectedSide === "left" ? "black" : "lightgray",
},
}}
>
{leftLabel}
</Button>
<Button
onClick={() => handleSwitch("right")}
sx={{
flex: 1,
backgroundColor: selectedSide === "right" ? "black" : "white",
color: selectedSide === "right" ? "white" : "black",
borderRadius: 0,
"&:hover": {
backgroundColor: selectedSide === "right" ? "black" : "lightgray",
},
}}
>
{rightLabel}
</Button>
</Box>
);
};
export default TwoSidedSwitch;
@@ -1,19 +0,0 @@
import * as React from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
export default function Loading() {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginY: 10,
width: 1,
}}
>
<CircularProgress />
</Box>
);
}
@@ -1,40 +0,0 @@
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
import { ICardDataRowsProps } from "./ExplorerCard";
export interface IStakingCardProps {
title?: string;
titleCenter?: string;
paragraphCenter?: string;
closeButton?: {
onClick: () => void;
};
addressInput?: boolean;
amountInput?: boolean;
dataRows?: ICardDataRowsProps;
blockExplorerLink?: string;
backButton?: {
onClick: () => void;
};
nextButton?: {
onClick: () => void;
};
}
export const StakingCard = (props: IStakingCardProps) => {
const {
title,
titleCenter,
closeButton,
paragraphCenter,
addressInput,
amountInput,
dataRows,
blockExplorerLink,
backButton,
nextButton,
} = props;
return (
<Card sx={{ height: "100%", borderRadius: "unset" }}>
<CardContent></CardContent>
</Card>
);
};
-37
View File
@@ -1,37 +0,0 @@
import * as React from "react";
import { useTheme } from "@mui/material/styles";
export const NymTokenSVG: FCWithChildren = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_7134_15888)">
<path
d="M17.07 3.43C13.17 -0.480002 6.83 -0.480002 2.93 3.43C-0.980002 7.34 -0.980002 13.67 2.93 17.57C6.84 21.48 13.17 21.48 17.07 17.57C20.98 13.67 20.98 7.33 17.07 3.43ZM16.21 16.71C12.78 20.14 7.21 20.14 3.78 16.71C0.349997 13.28 0.349997 7.71 3.78 4.28C7.21 0.849997 12.78 0.849997 16.21 4.28C19.65 7.72 19.65 13.28 16.21 16.71Z"
fill={color}
/>
<path
d="M15.4 16.33V4.66999C14.89 4.18999 14.32 3.76999 13.71 3.43999V14.59L6.35001 3.39999C5.71001 3.73999 5.12001 4.15999 4.60001 4.65999V16.33C5.11001 16.81 5.68001 17.23 6.29001 17.56V6.40999L13.65 17.6C14.29 17.26 14.88 16.83 15.4 16.33Z"
fill={color}
/>
</g>
<defs>
<clipPath id="clip0_7134_15888">
<rect
width="20"
height="20"
fill={color}
transform="translate(0 0.5)"
/>
</clipPath>
</defs>
</svg>
);
};
+7 -7
View File
@@ -1,15 +1,15 @@
import type { Metadata } from "next";
import "@interchain-ui/react/styles";
import { App } from "./App";
import type { Metadata } from 'next'
import '@interchain-ui/react/styles'
import { App } from './App'
export const metadata: Metadata = {
title: "Nym Network Explorer",
};
title: 'Nym Network Explorer',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html lang="en">
@@ -17,5 +17,5 @@ export default function RootLayout({
<App>{children}</App>
</body>
</html>
);
)
}
@@ -1,72 +1,72 @@
"use client";
'use client'
import * as React from "react";
import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material";
import { useParams } from "next/navigation";
import { GatewayBond } from "@/app/typeDefs/explorer-api";
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
import * as React from 'react'
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
import { useParams } from 'next/navigation'
import { GatewayBond } from '@/app/typeDefs/explorer-api'
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
import {
gatewayEnrichedToGridRow,
GatewayEnrichedRowType,
} from "@/app/components/Gateways/Gateways";
import { ComponentError } from "@/app/components/ComponentError";
import { ContentCard } from "@/app/components/ContentCard";
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
import { UptimeChart } from "@/app/components/UptimeChart";
} from '@/app/components/Gateways/Gateways'
import { ComponentError } from '@/app/components/ComponentError'
import { ContentCard } from '@/app/components/ContentCard'
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
import { UptimeChart } from '@/app/components/UptimeChart'
import {
GatewayContextProvider,
useGatewayContext,
} from "@/app/context/gateway";
import { useMainContext } from "@/app/context/main";
import { Title } from "@/app/components/Title";
} from '@/app/context/gateway'
import { useMainContext } from '@/app/context/main'
import { Title } from '@/app/components/Title'
const columns: ColumnsType[] = [
{
field: "identity_key",
title: "Identity Key",
headerAlign: "left",
field: 'identity_key',
title: 'Identity Key',
headerAlign: 'left',
width: 230,
},
{
field: "bond",
title: "Bond",
headerAlign: "left",
field: 'bond',
title: 'Bond',
headerAlign: 'left',
},
{
field: "node_performance",
title: "Routing Score",
headerAlign: "left",
field: 'node_performance',
title: 'Routing Score',
headerAlign: 'left',
tooltipInfo:
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
},
{
field: "avgUptime",
title: "Avg. Score",
headerAlign: "left",
field: 'avgUptime',
title: 'Avg. Score',
headerAlign: 'left',
tooltipInfo: "Gateway's average routing score in the last 24 hours",
},
{
field: "host",
title: "IP",
headerAlign: "left",
field: 'host',
title: 'IP',
headerAlign: 'left',
width: 99,
},
{
field: "location",
title: "Location",
headerAlign: "left",
field: 'location',
title: 'Location',
headerAlign: 'left',
},
{
field: "owner",
title: "Owner",
headerAlign: "left",
field: 'owner',
title: 'Owner',
headerAlign: 'left',
},
{
field: "version",
title: "Version",
headerAlign: "left",
field: 'version',
title: 'Version',
headerAlign: 'left',
},
];
]
/**
* Shows gateway details
@@ -74,26 +74,26 @@ const columns: ColumnsType[] = [
const PageGatewayDetailsWithState = ({
selectedGateway,
}: {
selectedGateway: GatewayBond | undefined;
selectedGateway: GatewayBond | undefined
}) => {
const [enrichGateway, setEnrichGateway] =
React.useState<GatewayEnrichedRowType>();
const [status, setStatus] = React.useState<number[] | undefined>();
const { uptimeReport, uptimeStory } = useGatewayContext();
React.useState<GatewayEnrichedRowType>()
const [status, setStatus] = React.useState<number[] | undefined>()
const { uptimeReport, uptimeStory } = useGatewayContext()
React.useEffect(() => {
if (uptimeReport?.data && selectedGateway) {
setEnrichGateway(
gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)
);
)
}
}, [uptimeReport, selectedGateway]);
}, [uptimeReport, selectedGateway])
React.useEffect(() => {
if (enrichGateway) {
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort])
}
}, [enrichGateway]);
}, [enrichGateway])
return (
<Box component="main">
@@ -115,7 +115,7 @@ const PageGatewayDetailsWithState = ({
<ContentCard title="Gateway Status">
<TwoColSmallTable
loading={false}
keys={["Mix port", "Client WS API Port"]}
keys={['Mix port', 'Client WS API Port']}
values={status.map((each) => each)}
icons={status.map((elem) => !!elem)}
/>
@@ -137,40 +137,39 @@ const PageGatewayDetailsWithState = ({
</ContentCard>
)}
</Grid>
<Grid item xs={12} md={8}></Grid>
</Grid>
</Box>
);
};
)
}
/**
* Guard component to handle loadingW and not found states
*/
const PageGatewayDetailGuard = () => {
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
const { gateways } = useMainContext();
const { id } = useParams();
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>()
const { gateways } = useMainContext()
const { id } = useParams()
React.useEffect(() => {
if (gateways?.data) {
setSelectedGateway(
gateways.data.find((g) => g.gateway.identity_key === id)
);
)
}
}, [gateways, id]);
}, [gateways, id])
if (gateways?.isLoading) {
return <CircularProgress />;
return <CircularProgress />
}
if (gateways?.error) {
// eslint-disable-next-line no-console
console.error(gateways?.error);
console.error(gateways?.error)
return (
<Alert severity="error">
Oh no! Could not load mixnode <code>{id || ""}</code>
Oh no! Could not load mixnode <code>{id || ''}</code>
</Alert>
);
)
}
// loaded, but not found
@@ -178,31 +177,31 @@ const PageGatewayDetailGuard = () => {
return (
<Alert severity="warning">
<AlertTitle>Gateway not found</AlertTitle>
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
</Alert>
);
)
}
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
};
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />
}
/**
* Wrapper component that adds the mixnode content based on the `id` in the address URL
*/
const PageGatewayDetail = () => {
const { id } = useParams();
const { id } = useParams()
if (!id || typeof id !== "string") {
if (!id || typeof id !== 'string') {
return (
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
);
)
}
return (
<GatewayContextProvider gatewayIdentityKey={id}>
<PageGatewayDetailGuard />
</GatewayContextProvider>
);
};
)
}
export default PageGatewayDetail;
export default PageGatewayDetail
@@ -1,6 +1,6 @@
"use client";
'use client'
import * as React from "react";
import * as React from 'react'
import {
Alert,
AlertTitle,
@@ -8,82 +8,75 @@ import {
CircularProgress,
Grid,
Typography,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
import { BondBreakdownTable } from "@/app/components/MixNodes/BondBreakdown";
} from '@mui/material'
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown'
import {
DelegatorsInfoTable,
EconomicsInfoColumns,
EconomicsInfoRows,
} from "@/app/components/MixNodes/Economics";
import { ComponentError } from "@/app/components/ComponentError";
import { ContentCard } from "@/app/components/ContentCard";
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
import { UptimeChart } from "@/app/components/UptimeChart";
import { WorldMap } from "@/app/components/WorldMap";
import { MixNodeDetailSection } from "@/app/components/MixNodes/DetailSection";
} from '@/app/components/MixNodes/Economics'
import { ComponentError } from '@/app/components/ComponentError'
import { ContentCard } from '@/app/components/ContentCard'
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
import { UptimeChart } from '@/app/components/UptimeChart'
import { WorldMap } from '@/app/components/WorldMap'
import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection'
import {
MixnodeContextProvider,
useMixnodeContext,
} from "@/app/context/mixnode";
import { Title } from "@/app/components/Title";
import { useIsMobile } from "@/app/hooks/useIsMobile";
import { useParams } from "next/navigation";
import Script from "next/script";
import Head from "next/head";
import { useEffect } from "react";
import { useMainContext } from "@/app/context/main";
import { ExplorerCard } from "@/app/components/ExplorerCard";
} from '@/app/context/mixnode'
import { Title } from '@/app/components/Title'
import { useIsMobile } from '@/app/hooks/useIsMobile'
import { useParams } from 'next/navigation'
const columns: ColumnsType[] = [
{
field: "owner",
title: "Owner",
width: "15%",
field: 'owner',
title: 'Owner',
width: '15%',
},
{
field: "identity_key",
title: "Identity Key",
width: "15%",
field: 'identity_key',
title: 'Identity Key',
width: '15%',
},
{
field: "bond",
title: "Stake",
width: "12.5%",
field: 'bond',
title: 'Stake',
width: '12.5%',
},
{
field: "stake_saturation",
title: "Stake Saturation",
width: "12.5%",
field: 'stake_saturation',
title: 'Stake Saturation',
width: '12.5%',
tooltipInfo:
"Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.",
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
},
{
field: "self_percentage",
width: "10%",
title: "Bond %",
field: 'self_percentage',
width: '10%',
title: 'Bond %',
tooltipInfo:
"Percentage of the operator's bond to the total stake on the node",
},
{
field: "host",
width: "10%",
title: "Host",
field: 'host',
width: '10%',
title: 'Host',
},
{
field: "location",
title: "Location",
field: 'location',
title: 'Location',
},
{
field: "layer",
title: "Layer",
field: 'layer',
title: 'Layer',
},
];
]
/**
* Shows mix node details
@@ -97,53 +90,10 @@ const PageMixnodeDetailWithState = () => {
status,
uptimeStory,
uniqDelegations,
} = useMixnodeContext();
const isMobile = useIsMobile();
const { mode } = useMainContext();
// useEffect(() => {
// if (typeof window !== "undefined") {
// // Set Remark42 configuration on the window object
// window.remark_config = {
// host: "http://localhost:8081",
// site_id: "remark42",
// components: ["embed", "last-comments"],
// max_shown_comments: 100,
// theme: mode === "light" ? "light" : "dark",
// page_title: "My custom title for a page",
// locale: "en",
// show_email_subscription: false,
// simple_view: true,
// no_footer: true,
// };
// // Dynamically load the Remark42 script if it doesn't exist
// if (!document.getElementById("remark42-script")) {
// const script = document.createElement("script");
// script.src = `${window.remark_config.host}/web/embed.js`;
// script.async = true;
// script.defer = true;
// script.id = "remark42-script";
// document.body.appendChild(script);
// } else if (window.REMARK42) {
// // Re-initialize if the script is already loaded
// window.REMARK42.createInstance(window.remark_config);
// }
// }
// }, []);
// // React to mode changes and update Remark42 theme
// useEffect(() => {
// if (window.REMARK42 && window.REMARK42.changeTheme) {
// window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
// }
// }, [mode]);
} = useMixnodeContext()
const isMobile = useIsMobile()
return (
<Box component="main">
<Head>
<title>Mixnode Detail</title>
</Head>
<Title text="Mixnode Detail" />
<Grid container spacing={2} mt={1} mb={6}>
<Grid item xs={12}>
@@ -155,9 +105,9 @@ const PageMixnodeDetailWithState = () => {
)}
{mixNodeRow?.blacklisted && (
<Typography
textAlign={isMobile ? "left" : "right"}
textAlign={isMobile ? 'left' : 'right'}
fontSize="smaller"
sx={{ color: "error.main" }}
sx={{ color: 'error.main' }}
>
This node is having a poor performance
</Typography>
@@ -203,7 +153,7 @@ const PageMixnodeDetailWithState = () => {
loading={stats.isLoading}
error={stats?.error?.message}
title="Since startup"
keys={["Received", "Sent", "Explicitly dropped"]}
keys={['Received', 'Sent', 'Explicitly dropped']}
values={[
stats?.data?.packets_received_since_startup || 0,
stats?.data?.packets_sent_since_startup || 0,
@@ -214,7 +164,7 @@ const PageMixnodeDetailWithState = () => {
loading={stats.isLoading}
error={stats?.error?.message}
title="Since last update"
keys={["Received", "Sent", "Explicitly dropped"]}
keys={['Received', 'Sent', 'Explicitly dropped']}
values={[
stats?.data?.packets_received_since_last_update || 0,
stats?.data?.packets_sent_since_last_update || 0,
@@ -254,7 +204,7 @@ const PageMixnodeDetailWithState = () => {
<TwoColSmallTable
loading={status.isLoading}
error={status?.error?.message}
keys={["Mix port", "Verloc port", "HTTP port"]}
keys={['Mix port', 'Verloc port', 'HTTP port']}
values={[1789, 1790, 8000].map((each) => each)}
icons={
(status?.data?.ports && Object.values(status.data.ports)) || [
@@ -287,32 +237,29 @@ const PageMixnodeDetailWithState = () => {
)}
</Grid>
</Grid>
<Grid item xs={12} md={4}>
<ExplorerCard chat={true} overTitle="Test" />
</Grid>
</Box>
);
};
)
}
/**
* Guard component to handle loading and not found states
*/
const PageMixnodeDetailGuard = () => {
const { mixNode } = useMixnodeContext();
const { id } = useParams();
const { mixNode } = useMixnodeContext()
const { id } = useParams()
if (mixNode?.isLoading) {
return <CircularProgress />;
return <CircularProgress />
}
if (mixNode?.error) {
// eslint-disable-next-line no-console
console.error(mixNode?.error);
console.error(mixNode?.error)
return (
<Alert severity="error">
Oh no! Could not load mixnode <code>{id || ""}</code>
Oh no! Could not load mixnode <code>{id || ''}</code>
</Alert>
);
)
}
// loaded, but not found
@@ -320,31 +267,31 @@ const PageMixnodeDetailGuard = () => {
return (
<Alert severity="warning">
<AlertTitle>Mixnode not found</AlertTitle>
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
</Alert>
);
)
}
return <PageMixnodeDetailWithState />;
};
return <PageMixnodeDetailWithState />
}
/**
* Wrapper component that adds the mixnode content based on the `id` in the address URL
*/
const PageMixnodeDetail = () => {
const { id } = useParams();
const { id } = useParams()
if (!id || typeof id !== "string") {
if (!id || typeof id !== 'string') {
return (
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
);
)
}
return (
<MixnodeContextProvider mixId={id}>
<PageMixnodeDetailGuard />
</MixnodeContextProvider>
);
};
)
}
export default PageMixnodeDetail;
export default PageMixnodeDetail
+33 -313
View File
@@ -1,288 +1,26 @@
"use client";
'use client'
import React, { useEffect, useState } from "react";
import { Box, Grid, Link, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { PeopleAlt } from "@mui/icons-material";
import { Title } from "@/app/components/Title";
import { StatsCard } from "@/app/components/StatsCard";
import { MixnodesSVG } from "@/app/icons/MixnodesSVG";
import { Icons } from "@/app/components/Icons";
import { GatewaysSVG } from "@/app/icons/GatewaysSVG";
import { ValidatorsSVG } from "@/app/icons/ValidatorsSVG";
import { ContentCard } from "@/app/components/ContentCard";
import { WorldMap } from "@/app/components/WorldMap";
import { BIG_DIPPER } from "@/app/api/constants";
import { formatNumber } from "@/app/utils";
import { useMainContext } from "./context/main";
import { useRouter } from "next/navigation";
import { ContentCardProps, ExplorerCard } from "./components/ExplorerCard";
import { ExplorerData, getCacheExplorerData } from "./api/explorer";
import { IExplorerLineChartData } from "./components/ExplorerLineChart";
import {
AccountStatsCard,
IAccountStatsCardProps,
} from "./components/AccountStatsCard";
import TwoSidedSwitch from "./components/ExplorerSwitchButton";
import React, { useEffect } from 'react'
import { Box, Grid, Link, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import { PeopleAlt } from '@mui/icons-material'
import { Title } from '@/app/components/Title'
import { StatsCard } from '@/app/components/StatsCard'
import { MixnodesSVG } from '@/app/icons/MixnodesSVG'
import { Icons } from '@/app/components/Icons'
import { GatewaysSVG } from '@/app/icons/GatewaysSVG'
import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG'
import { ContentCard } from '@/app/components/ContentCard'
import { WorldMap } from '@/app/components/WorldMap'
import { BIG_DIPPER } from '@/app/api/constants'
import { formatNumber } from '@/app/utils'
import { useMainContext } from './context/main'
import { useRouter } from 'next/navigation'
const explorerCard: ContentCardProps = {
overTitle: "SINGLE",
profileImage: {},
title: "SINGLE",
profileCountry: {
countryCode: "NO",
countryName: "Norway",
},
upDownLine: {
percentage: 10,
numberWentUp: true,
},
titlePrice: {
price: 1.15,
upDownLine: {
percentage: 10,
numberWentUp: true,
},
},
dataRows: {
rows: [
{ key: "Market cap", value: "$ 1000000" },
{ key: "24H VOL", value: "$ 1000000" },
],
},
graph: {
data: [
{
date_utc: "2024-11-20",
numericData: 10,
},
{
date_utc: "2024-11-21",
numericData: 12,
},
{
date_utc: "2024-11-22",
numericData: 9,
},
{
date_utc: "2024-11-23",
numericData: 11,
},
],
color: "#00CA33",
label: "Label",
},
nymAddress: {
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
title: "Nym address",
},
identityKey: {
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
title: "Nym address",
},
qrCode: {
url: "https://nymtech.net",
},
ratings: {
ratings: [
{ title: "Rating", numberOfStars: 4 },
{ title: "Rating", numberOfStars: 2 },
{ title: "Rating", numberOfStars: 3 },
],
},
chat: true,
paragraph: "Additional line",
button: {
onClick: () => {},
label: "Label",
},
};
export const DATA_REVALIDATE = 60;
export default function PageOverview() {
const [explorerData, setExplorerData] = useState<ExplorerData | null>(null);
const [noiseLineGraphData, setNoiseLineGraphData] = useState<{
color: string;
label: string;
data: IExplorerLineChartData[];
}>();
const [stakeLineGraphData, setStakeLineGraphData] = useState<{
color: string;
label: string;
data: IExplorerLineChartData[];
}>();
useEffect(() => {
async function fetchData() {
const data = await getCacheExplorerData();
setExplorerData(data);
}
fetchData();
}, []);
const theme = useTheme();
const router = useRouter();
// CURRENT EPOCH
const currentEpochStart =
explorerData?.currentEpochData.current_epoch_start || "";
const progressBar = {
title: "Current NGM epoch",
start: currentEpochStart || "",
showEpoch: true,
};
const formatBigNum = (num: number) => {
if (typeof num === "number") {
if (num >= 1000000000) {
return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B";
}
if (num >= 1000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
}
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
}
return num;
}
};
// NOISE
const noiseLast24H =
explorerData?.packetsAndStakingData[
explorerData.packetsAndStakingData.length - 1
].total_packets_sent +
explorerData?.packetsAndStakingData[
explorerData.packetsAndStakingData.length - 1
].total_packets_received;
const noisePrevious24H =
explorerData?.packetsAndStakingData[
explorerData.packetsAndStakingData.length - 2
].total_packets_sent +
explorerData?.packetsAndStakingData[
explorerData.packetsAndStakingData.length - 2
].total_packets_received;
const calculatePercentageChange = (last24H: number, previous24H: number) => {
if (previous24H === 0) {
throw new Error(
"Cannot calculate percentage change when yesterday's value is zero."
);
}
const change = ((last24H - previous24H) / previous24H) * 100;
return parseFloat(change.toFixed(2));
};
const percentage = calculatePercentageChange(noiseLast24H, noisePrevious24H);
const getPacketsData = () => {
const data: Array<IExplorerLineChartData> = [];
explorerData?.packetsAndStakingData.map((item: any) => {
data.push({
date_utc: item.date_utc,
numericData: item.total_packets_sent + item.total_packets_received,
});
});
return data;
};
useEffect(() => {
const noiseLineGraphData = {
color: "#8482FD",
label: "Total packets sent and received",
data: getPacketsData(),
};
setNoiseLineGraphData(noiseLineGraphData);
}, [explorerData]);
const noiseCard = {
overTitle: "Noise generated last 24h",
title: formatBigNum(noiseLast24H) || "",
upDownLine: {
percentage: Math.abs(percentage) || 0,
numberWentUp: percentage > 0,
},
graph: noiseLineGraphData,
};
// STAKE
const currentStake =
Number(explorerData?.currentEpochRewardsData.interval.staking_supply) /
1000000 || 0;
const getStakeData = () => {
const data: Array<IExplorerLineChartData> = [];
explorerData?.packetsAndStakingData.map((item: any) => {
data.push({
date_utc: item.date_utc,
numericData: item.total_stake / 1000000,
});
});
return data;
};
useEffect(() => {
const stakeLineGraphData = {
color: "#00CA33",
label: "Total stake delegated in NYM",
data: getStakeData(),
};
setStakeLineGraphData(stakeLineGraphData);
}, [explorerData]);
const stakeCard = {
overTitle: "Current network stake",
title: currentStake + " NYM" || "",
graph: stakeLineGraphData,
};
const accountStatsCard: IAccountStatsCardProps = {
overTitle: "Total value",
priceTitle: 1990.0174,
rows: [
{ type: "Spendable", allocation: 15.53, amount: 12800, value: 1200 },
{
type: "Delegated",
allocation: 15.53,
amount: 12800,
value: 1200,
history: [
{ type: "Liquid", amount: 6900 },
{ type: "Locked", amount: 6900 },
],
},
{
type: "Claimable",
allocation: 15.53,
amount: 12800,
value: 1200,
history: [
{ type: "Unlocked", amount: 6900 },
{ type: "Staking rewards", amount: 6900 },
{ type: "Operator comission", amount: 6900 },
],
},
{
type: "Self bonded",
allocation: 15.53,
amount: 12800,
value: 1200,
},
{
type: "Locked",
allocation: 15.53,
amount: 12800,
value: 1200,
},
],
};
const PageOverview = () => {
const theme = useTheme()
const router = useRouter()
const {
summaryOverview,
@@ -291,14 +29,9 @@ export default function PageOverview() {
block,
countryData,
serviceProviders,
} = useMainContext();
} = useMainContext()
return (
<Box component="main" sx={{ flexGrow: 1 }}>
<TwoSidedSwitch
leftLabel="Account"
rightLabel="Mixnode"
onSwitch={() => {}}
/>
<Grid>
<Grid item paddingBottom={3}>
<Title text="Overview" />
@@ -307,34 +40,19 @@ export default function PageOverview() {
<Grid container spacing={3}>
{summaryOverview && (
<>
<Grid item xs={12}>
<AccountStatsCard {...accountStatsCard} />
</Grid>
<Grid item xs={12} md={4}>
<ExplorerCard {...explorerCard} />
</Grid>
<Grid item xs={12} md={4}>
<ExplorerCard progressBar={progressBar} />
</Grid>
<Grid item xs={12} md={4}>
<ExplorerCard {...noiseCard} />
</Grid>
<Grid item xs={12} md={4}>
<ExplorerCard {...stakeCard} />
</Grid>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push("/network-components/mixnodes")}
onClick={() => router.push('/network-components/mixnodes')}
title="Mixnodes"
icon={<MixnodesSVG />}
count={summaryOverview.data?.mixnodes.count || ""}
count={summaryOverview.data?.mixnodes.count || ''}
errorMsg={summaryOverview?.error}
/>
</Grid>
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push("/network-components/mixnodes?status=active")
router.push('/network-components/mixnodes?status=active')
}
title="Active nodes"
icon={<Icons.Mixnodes.Status.Active />}
@@ -348,7 +66,7 @@ export default function PageOverview() {
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push("/network-components/mixnodes?status=standby")
router.push('/network-components/mixnodes?status=standby')
}
title="Standby nodes"
color={
@@ -364,9 +82,9 @@ export default function PageOverview() {
{gateways && (
<Grid item xs={12} md={4}>
<StatsCard
onClick={() => router.push("/network-components/gateways")}
onClick={() => router.push('/network-components/gateways')}
title="Gateways"
count={gateways?.data?.length || ""}
count={gateways?.data?.length || ''}
errorMsg={gateways?.error}
icon={<GatewaysSVG />}
/>
@@ -376,7 +94,7 @@ export default function PageOverview() {
<Grid item xs={12} md={4}>
<StatsCard
onClick={() =>
router.push("/network-components/service-providers")
router.push('/network-components/service-providers')
}
title="Service providers"
icon={<PeopleAlt />}
@@ -390,7 +108,7 @@ export default function PageOverview() {
<StatsCard
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
title="Validators"
count={validators?.data?.count || ""}
count={validators?.data?.count || ''}
errorMsg={validators?.error}
icon={<ValidatorsSVG />}
/>
@@ -432,5 +150,7 @@ export default function PageOverview() {
</Grid>
</Grid>
</Box>
);
)
}
export default PageOverview
-1
View File
@@ -1 +0,0 @@
declare module "react-world-flags";
-46
View File
@@ -1,46 +0,0 @@
version: "2"
services:
remark:
# remove the next line in case you want to use this Docker Compose file separately
# as otherwise it would complain for absence of Dockerfile
build: .
image: umputun/remark42:latest
container_name: "explorer_remark42"
hostname: "remark42"
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# uncomment to expose directly (no proxy)
ports:
- "8081:8080"
- "443:8443"
environment:
# - REMARK_URL=http://localhost:8081
- REMARK_URL=https://remark.blockfend.com
- SECRET=secret-key
- AUTH_ANON=true
# - SITE=remark42
- SITE=nym-explorer-test
# - DEBUG=true
# - AUTH_GOOGLE_CID
# - AUTH_GOOGLE_CSEC
# - AUTH_GITHUB_CID
# - AUTH_GITHUB_CSEC
# - AUTH_FACEBOOK_CID
# - AUTH_FACEBOOK_CSEC
# - AUTH_DISQUS_CID
# - AUTH_DISQUS_CSEC
# Enable it only for the initial comment import or for manual backups.
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
# to keep creating backups manually!
- ADMIN_PASSWD=password
volumes:
- ./var:/srv/var
+4 -7
View File
@@ -9,18 +9,15 @@
"lint": "next lint"
},
"dependencies": {
"@mui/x-data-grid": "7.1.1",
"@mui/x-date-pickers": "7.1.1",
"@nivo/line": "^0.88.0",
"@nymproject/nym-validator-client": "0.18.0",
"@nymproject/react": "^1.0.0",
"material-react-table": "^2.12.1",
"@nymproject/nym-validator-client": "0.18.0",
"next": "14.1.4",
"react": "^18",
"react-dom": "^18",
"react-error-boundary": "^4.0.13",
"react-world-flags": "^1.6.0",
"qrcode.react": "^4.1.0"
"material-react-table": "^2.12.1",
"@mui/x-date-pickers": "7.1.1",
"@mui/x-data-grid": "7.1.1"
},
"devDependencies": {
"@types/node": "^20",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.
+1 -2
View File
@@ -28,8 +28,7 @@
"../assets/*"
]
},
"moduleResolution": "node",
"target": "ES2017"
"moduleResolution": "node"
},
"include": [
"next-env.d.ts",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

Binary file not shown.
Binary file not shown.
-2
View File
@@ -77,11 +77,9 @@ nym-network-defaults = { path = "../common/network-defaults" }
nym-network-requester = { path = "../service-providers/network-requester" }
nym-node-http-api = { path = "../nym-node/nym-node-http-api" }
nym-pemstore = { path = "../common/pemstore" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-sphinx = { path = "../common/nymsphinx" }
nym-statistics-common = { path = "../common/statistics" }
nym-task = { path = "../common/task" }
nym-topology = { path = "../common/topology" }
nym-types = { path = "../common/types" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
+2 -60
View File
@@ -3,19 +3,14 @@
use crate::config::Config;
use crate::error::GatewayError;
use async_trait::async_trait;
use nym_crypto::asymmetric::encryption;
use nym_gateway_stats_storage::PersistentStatsStorage;
use nym_gateway_storage::PersistentStorage;
use nym_pemstore::traits::PemStorableKeyPair;
use nym_pemstore::KeyPairPath;
use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
use nym_topology::{gateway, NymTopology, TopologyProvider};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::debug;
use url::Url;
pub async fn load_network_requester_config<P: AsRef<Path>>(
id: &str,
@@ -107,56 +102,3 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result<encryption::KeyPair, G
);
load_keypair(sphinx_paths, "gateway sphinx")
}
#[derive(Clone)]
pub struct GatewayTopologyProvider {
inner: Arc<Mutex<GatewayTopologyProviderInner>>,
}
impl GatewayTopologyProvider {
pub fn new(
gateway_node: gateway::LegacyNode,
user_agent: UserAgent,
nym_api_url: Vec<Url>,
) -> GatewayTopologyProvider {
GatewayTopologyProvider {
inner: Arc::new(Mutex::new(GatewayTopologyProviderInner {
inner: NymApiTopologyProvider::new(
NymApiTopologyProviderConfig {
min_mixnode_performance: 50,
min_gateway_performance: 0,
},
nym_api_url,
env!("CARGO_PKG_VERSION").to_string(),
Some(user_agent),
),
gateway_node,
})),
}
}
}
struct GatewayTopologyProviderInner {
inner: NymApiTopologyProvider,
gateway_node: gateway::LegacyNode,
}
#[async_trait]
impl TopologyProvider for GatewayTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let mut guard = self.inner.lock().await;
match guard.inner.get_new_topology().await {
None => None,
Some(mut base) => {
if !base.gateway_exists(&guard.gateway_node.identity_key) {
debug!(
"{} didn't exist in topology. inserting it.",
guard.gateway_node.identity_key
);
base.insert_gateway(guard.gateway_node.clone());
}
Some(base)
}
}
}
}
+1 -50
View File
@@ -14,11 +14,9 @@ use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle,
use crate::node::client_handling::websocket;
use crate::node::helpers::{
initialise_main_storage, initialise_stats_storage, load_network_requester_config,
GatewayTopologyProvider,
};
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
use futures::channel::{mpsc, oneshot};
use nym_bin_common::bin_info;
use nym_credential_verification::ecash::{
credential_sender::CredentialHandlerConfig, EcashManager,
};
@@ -29,15 +27,13 @@ use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilte
use nym_node_http_api::state::metrics::SharedSessionStats;
use nym_statistics_common::events::{self, StatsEventSender};
use nym_task::{TaskClient, TaskHandle, TaskManager};
use nym_topology::NetworkAddress;
use nym_types::gateway::GatewayNodeDetailsResponse;
use nym_validator_client::client::NodeId;
use nym_validator_client::nyxd::{Coin, CosmWasmClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use rand::seq::SliceRandom;
use rand::thread_rng;
use statistics::GatewayStatisticsCollector;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::*;
@@ -240,39 +236,6 @@ impl<St> Gateway<St> {
crate::helpers::node_details(&self.config).await
}
fn gateway_topology_provider(&self) -> GatewayTopologyProvider {
GatewayTopologyProvider::new(
self.as_topology_node(),
bin_info!().into(),
self.config.gateway.nym_api_urls.clone(),
)
}
fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode {
let ip = self
.config
.host
.public_ips
.first()
.copied()
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port);
nym_topology::gateway::LegacyNode {
// those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate.
// the only thing that matters is the identity key (and maybe version)
node_id: NodeId::MAX,
mix_host,
host: NetworkAddress::IpAddr(ip),
clients_ws_port: self.config.gateway.clients_port,
clients_wss_port: self.config.gateway.clients_wss_port,
sphinx_key: *self.sphinx_keypair.public_key(),
identity_key: *self.identity_keypair.public_key(),
version: env!("CARGO_PKG_VERSION").into(),
}
}
fn start_mix_socket_listener(
&self,
ack_sender: MixForwardingSender,
@@ -305,7 +268,6 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&mut self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>>
@@ -353,7 +315,6 @@ impl<St> Gateway<St> {
.with_shutdown(shutdown.fork("authenticator"))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &opts.custom_mixnet_path {
@@ -402,7 +363,6 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&self,
_forwarding_channel: MixForwardingSender,
_topology_provider: GatewayTopologyProvider,
_shutdown: TaskClient,
_ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> {
@@ -475,7 +435,6 @@ impl<St> Gateway<St> {
async fn start_network_requester(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<StartedNetworkRequester, GatewayError> {
info!("Starting network requester...");
@@ -503,7 +462,6 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path {
@@ -541,7 +499,6 @@ impl<St> Gateway<St> {
async fn start_ip_packet_router(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<LocalEmbeddedClientHandle, GatewayError> {
info!("Starting IP packet provider...");
@@ -570,7 +527,6 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path {
@@ -687,8 +643,6 @@ impl<St> Gateway<St> {
shutdown.fork("statistics::GatewayStatisticsCollector"),
);
let topology_provider = self.gateway_topology_provider();
let handler_config = CredentialHandlerConfig {
revocation_bandwidth_penalty: self
.config
@@ -737,7 +691,6 @@ impl<St> Gateway<St> {
let embedded_nr = self
.start_network_requester(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("NetworkRequester"),
)
.await?;
@@ -753,7 +706,6 @@ impl<St> Gateway<St> {
let embedded_ip_sp = self
.start_ip_packet_router(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("ip_service_provider"),
)
.await?;
@@ -766,7 +718,6 @@ impl<St> Gateway<St> {
let embedded_auth = self
.start_authenticator(
mix_forwarding_channel,
topology_provider,
shutdown.fork("authenticator"),
ecash_verifier,
)
+15 -3
View File
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmwasm_std::Decimal;
use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges;
use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding};
use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges;
use nym_mixnet_contract_common::{
GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@@ -62,7 +64,7 @@ pub struct LegacyMixNodeDetailsWithLayer {
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
#[serde(default)]
pub pending_changes: LegacyPendingMixNodeChanges,
pub pending_changes: PendingMixNodeChanges,
}
impl LegacyMixNodeDetailsWithLayer {
@@ -78,3 +80,13 @@ impl LegacyMixNodeDetailsWithLayer {
self.bond_information.is_unbonding
}
}
impl From<LegacyMixNodeDetailsWithLayer> for MixNodeDetails {
fn from(value: LegacyMixNodeDetailsWithLayer) -> Self {
MixNodeDetails {
bond_information: value.bond_information.into(),
rewarding_details: value.rewarding_details,
pending_changes: value.pending_changes,
}
}
}
+7 -53
View File
@@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::Wireguard;
@@ -138,48 +138,6 @@ pub struct NodePerformance {
pub last_24h: Performance,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
)]
pub enum DisplayRole {
EntryGateway,
Layer1,
Layer2,
Layer3,
ExitGateway,
Standby,
}
impl From<Role> for DisplayRole {
fn from(role: Role) -> Self {
match role {
Role::EntryGateway => DisplayRole::EntryGateway,
Role::Layer1 => DisplayRole::Layer1,
Role::Layer2 => DisplayRole::Layer2,
Role::Layer3 => DisplayRole::Layer3,
Role::ExitGateway => DisplayRole::ExitGateway,
Role::Standby => DisplayRole::Standby,
}
}
}
impl From<DisplayRole> for Role {
fn from(role: DisplayRole) -> Self {
match role {
DisplayRole::EntryGateway => Role::EntryGateway,
DisplayRole::Layer1 => Role::Layer1,
DisplayRole::Layer2 => Role::Layer2,
DisplayRole::Layer3 => Role::Layer3,
DisplayRole::ExitGateway => Role::ExitGateway,
DisplayRole::Standby => Role::Standby,
}
}
}
// imo for now there's no point in exposing more than that,
// nym-api shouldn't be calculating apy or stake saturation for you.
// it should just return its own metrics (performance) and then you can do with it as you wish
@@ -195,7 +153,7 @@ impl From<DisplayRole> for Role {
pub struct NodeAnnotation {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub last_24h_performance: Performance,
pub current_role: Option<DisplayRole>,
pub current_role: Option<Role>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
@@ -328,7 +286,7 @@ impl MixNodeBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
epoch_role: role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
@@ -387,7 +345,7 @@ impl GatewayBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
epoch_role: role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
@@ -852,10 +810,6 @@ impl NymNodeDescription {
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
@@ -873,7 +827,7 @@ impl NymNodeDescription {
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
epoch_role: role,
supported_roles: self.description.declared_role,
entry,
performance,
@@ -981,14 +935,14 @@ impl NymNodeData {
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedGateway {
pub bond: GatewayBond,
pub bond: LegacyGatewayBondWithId,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
fn from(bond: LegacyGatewayBondWithId) -> Self {
LegacyDescribedGateway {
bond: bond.bond,
bond,
self_described: None,
}
}
+3 -3
View File
@@ -141,8 +141,8 @@ pub struct SkimmedNode {
#[schemars(with = "String")]
pub x25519_sphinx_pubkey: x25519::PublicKey,
#[serde(alias = "epoch_role")]
pub role: NodeRole,
#[serde(alias = "role")]
pub epoch_role: NodeRole,
// needed for the purposes of sending appropriate test packets
#[serde(default)]
@@ -157,7 +157,7 @@ pub struct SkimmedNode {
impl SkimmedNode {
pub fn get_mix_layer(&self) -> Option<u8> {
match self.role {
match self.epoch_role {
NodeRole::Mixnode { layer } => Some(layer),
_ => None,
}
+6 -6
View File
@@ -24,21 +24,21 @@ use utoipa::IntoParams;
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
"/master-verification-key",
"/master-verification-key:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| master_verification_key(epoch_id, ecash_state)
}),
)
.route(
"/aggregated-expiration-date-signatures",
"/aggregated-expiration-date-signatures:expiration_date",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/aggregated-coin-indices-signatures",
"/aggregated-coin-indices-signatures:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
@@ -52,7 +52,7 @@ pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppStat
params(
EpochIdParam
),
path = "/v1/ecash/master-verification-key",
path = "/v1/ecash/master-verification-key/{epoch_id}",
responses(
(status = 200, body = VerificationKeyResponse)
)
@@ -83,7 +83,7 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/aggregated-expiration-date-signatures",
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
responses(
(status = 200, body = AggregatedExpirationDateSignatureResponse)
)
@@ -120,7 +120,7 @@ async fn expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/aggregated-coin-indices-signatures",
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
responses(
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
)
@@ -32,14 +32,14 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App
}),
)
.route(
"/partial-expiration-date-signatures",
"/partial-expiration-date-signatures:expiration_date",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/partial-coin-indices-signatures",
"/partial-coin-indices-signatures:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
@@ -127,7 +127,7 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/partial-expiration-date-signatures",
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
@@ -165,7 +165,7 @@ async fn partial_expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/partial-coin-indices-signatures",
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),

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