Compare commits

..

7 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu b83fdd34af Add fd callback for initial authentication 2025-03-21 15:07:54 +02:00
Bogdan-Ștefan Neacşu 356cd2eeac WIP 2025-03-21 13:53:37 +02:00
Jędrzej Stuczyński 0f6ec8610e hotfix: correctly increment ws connection counter (#5620) 2025-03-14 15:47:17 +00:00
benedetta davico c3b8c4b2f7 Merge pull request #5616 from nymtech/bd/remove-explorer-api-ci
Remove explorer-api from ci-build-binaries
2025-03-13 13:36:30 +01:00
benedettadavico 271b9e545c remove bump to explorer-api 2025-03-13 13:35:06 +01:00
benedetta davico 9641f01670 remove explorer-api from ci-build-binaries 2025-03-13 13:31:46 +01:00
benedettadavico a7bb3e8d91 bump versions for chokito 2025-03-13 13:19:37 +01:00
266 changed files with 5573 additions and 63763 deletions
@@ -100,7 +100,6 @@ jobs:
cp target/release/nymvisor $OUTPUT_DIR
cp target/release/nym-node $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
cp target/release/explorer-api $OUTPUT_DIR
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR
fi
Generated
+7 -10
View File
@@ -4786,7 +4786,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.53"
version = "1.1.54"
dependencies = [
"anyhow",
"async-trait",
@@ -5036,7 +5036,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.50"
version = "1.1.51"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5119,7 +5119,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.50"
version = "1.1.51"
dependencies = [
"bs58",
"clap",
@@ -6163,7 +6163,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.51"
version = "1.1.52"
dependencies = [
"addr",
"anyhow",
@@ -6214,7 +6214,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.6.2"
version = "1.7.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6601,7 +6601,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.50"
version = "1.1.51"
dependencies = [
"bs58",
"clap",
@@ -6933,7 +6933,6 @@ version = "0.1.0"
dependencies = [
"async-trait",
"nym-api-requests",
"nym-client-core-config-types",
"nym-config",
"nym-crypto",
"nym-mixnet-contract-common",
@@ -6945,8 +6944,6 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.12",
"time",
"tokio",
"tracing",
"tsify",
"wasm-bindgen",
@@ -7209,7 +7206,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.15"
version = "0.1.16"
dependencies = [
"anyhow",
"bytes",
-10
View File
@@ -67,13 +67,3 @@ As a general approach, licensing is as follows this pattern:
- documentation is Apache 2.0 or CC0-1.0
Nym Node Operators and Validators Terms and Conditions can be found [here](https://nym.com/operators-validators-terms).
## Getting Started
```bash
yarn install
```
```bash
yarn build
```
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.50"
version = "1.1.51"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.50"
version = "1.1.51"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -139,6 +139,8 @@ where
let gateway_setup = GatewaySetup::New {
specification: selection_spec,
available_gateways,
#[cfg(unix)]
connection_fd_callback: None,
};
let init_details =
@@ -187,6 +187,8 @@ where
let gateway_setup = GatewaySetup::New {
specification: selection_spec,
available_gateways,
#[cfg(unix)]
connection_fd_callback: None,
};
let init_details =
@@ -22,7 +22,7 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon
use crate::client::replies::reply_storage::{
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
};
use crate::client::topology_control::smart_api_provider::NymApiTopologyProvider;
use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider;
use crate::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
@@ -54,7 +54,8 @@ use nym_statistics_common::clients::ClientStatsSender;
use nym_statistics_common::generate_client_stats_id;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_task::{TaskClient, TaskHandle};
use nym_topology::providers::{HardcodedTopologyProvider, TopologyProvider};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::HardcodedTopologyProvider;
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use rand::rngs::OsRng;
use std::fmt::Debug;
@@ -556,7 +557,6 @@ where
config_topology,
nym_api_urls,
user_agent,
None,
)),
config::TopologyStructure::GeoAware(group_by) => {
warn!("using deprecated 'GeoAware' topology provider - this option will be removed very soon");
@@ -3,7 +3,7 @@ use log::{debug, error};
use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond};
use nym_network_defaults::var_names::EXPLORER_API;
use nym_topology::{
providers::{async_trait, TopologyProvider},
provider_trait::{async_trait, TopologyProvider},
NymTopology,
};
use nym_validator_client::client::NodeId;
@@ -19,12 +19,11 @@ use wasmtimer::tokio::sleep;
mod accessor;
pub mod geo_aware_provider;
pub mod nym_api_provider;
pub mod smart_api_provider;
#[allow(deprecated)]
pub use geo_aware_provider::GeoAwareTopologyProvider;
pub use nym_topology::providers::TopologyProvider;
pub use smart_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_topology::provider_trait::TopologyProvider;
// TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10;
@@ -3,7 +3,8 @@
use async_trait::async_trait;
use log::{debug, error, warn};
use nym_topology::{NymTopology, TopologyProvider};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopology;
use nym_validator_client::UserAgent;
use rand::prelude::SliceRandom;
use rand::thread_rng;
@@ -1,230 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
//! Caching, piecewise API Topology Provider
//!
#![warn(missing_docs)]
use async_trait::async_trait;
use log::{debug, error, warn};
pub use nym_topology::providers::piecewise::Config;
use nym_topology::{
providers::piecewise::{NymTopologyProvider, PiecewiseTopologyProvider},
EpochRewardedSet, NymTopology, RoutingNode, TopologyProvider,
};
use nym_validator_client::UserAgent;
use rand::{prelude::SliceRandom, thread_rng};
use url::Url;
/// Topology Provider build around a cached piecewise provider that uses the Nym API to
/// fetch changes and node details.
#[derive(Clone)]
pub struct NymApiTopologyProvider {
inner: NymTopologyProvider<NymApiPiecewiseProvider>,
}
impl NymApiTopologyProvider {
/// Construct a new thread safe Cached topology provider using the Nym API
pub fn new(
config: impl Into<Config>,
nym_api_urls: Vec<Url>,
user_agent: Option<UserAgent>,
initial_topology: Option<NymTopology>,
) -> Self {
let manager = NymApiPiecewiseProvider::new(nym_api_urls, user_agent);
let inner = NymTopologyProvider::new(manager, config.into(), initial_topology);
Self { inner }
}
}
impl AsRef<NymTopologyProvider<NymApiPiecewiseProvider>> for NymApiTopologyProvider {
fn as_ref(&self) -> &NymTopologyProvider<NymApiPiecewiseProvider> {
&self.inner
}
}
impl AsMut<NymTopologyProvider<NymApiPiecewiseProvider>> for NymApiTopologyProvider {
fn as_mut(&mut self) -> &mut NymTopologyProvider<NymApiPiecewiseProvider> {
&mut self.inner
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl TopologyProvider for NymApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.as_mut().get_new_topology().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl TopologyProvider for NymApiTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.as_mut().get_new_topology().await
}
}
#[derive(Clone)]
struct NymApiPiecewiseProvider {
validator_client: nym_validator_client::client::NymApiClient,
nym_api_urls: Vec<Url>,
currently_used_api: usize,
}
impl NymApiPiecewiseProvider {
fn new(mut nym_api_urls: Vec<Url>, user_agent: Option<UserAgent>) -> Self {
nym_api_urls.shuffle(&mut thread_rng());
let validator_client = if let Some(user_agent) = user_agent {
nym_validator_client::client::NymApiClient::new_with_user_agent(
nym_api_urls[0].clone(),
user_agent,
)
} else {
nym_validator_client::client::NymApiClient::new(nym_api_urls[0].clone())
};
Self {
validator_client,
nym_api_urls,
currently_used_api: 0,
}
}
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
return;
}
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
self.validator_client
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
}
async fn get_full_topology_inner(&mut self) -> Option<NymTopology> {
let layer_assignments = self.get_layer_assignments().await?;
let mut topology = NymTopology::new_empty(layer_assignments);
let all_nodes = self
.validator_client
.get_all_basic_nodes()
.await
.inspect_err(|err| {
self.use_next_nym_api();
error!("failed to get network nodes: {err}");
})
.ok()?;
debug!("there are {} nodes on the network", all_nodes.len());
topology.add_additional_nodes(all_nodes.iter());
if !topology.is_minimally_routable() {
error!("the current filtered active topology can't be used to construct any packets");
return None;
}
Some(topology)
}
async fn get_descriptor_batch_inner(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>> {
// Does this need to return a hashmap of RoutingNodes? that is moderately inconvenient
// especially when the nodes themselves contain their node_id unless we expect to directly
// use the result of this fn for lookups where we would otherwise for example, have to
// iterate over a whole vec to find a specific node_id.
let descriptor_vec = self
.validator_client
.retrieve_basic_nodes_batch(ids)
.await
.inspect_err(|err| {
self.use_next_nym_api();
error!("failed to get current rewarded set: {err}");
})
.ok()?;
let mut out = Vec::new();
for node in descriptor_vec {
if let Ok(routing_node) = RoutingNode::try_from(&node) {
out.push(routing_node);
}
}
Some(out)
}
async fn get_layer_assignments_inner(&mut self) -> Option<EpochRewardedSet> {
self.validator_client
.get_current_rewarded_set()
.await
.inspect_err(|err| {
self.use_next_nym_api();
error!("failed to get current rewarded set: {err}");
})
.ok()
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl PiecewiseTopologyProvider for NymApiPiecewiseProvider {
async fn get_full_topology(&mut self) -> Option<NymTopology> {
self.get_full_topology_inner().await
}
async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>> {
self.get_descriptor_batch_inner(ids).await
}
async fn get_layer_assignments(&mut self) -> Option<EpochRewardedSet> {
self.get_layer_assignments_inner().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl PiecewiseTopologyProvider for NymApiPiecewiseProvider {
async fn get_full_topology(&mut self) -> Option<NymTopology> {
self.get_full_topology_inner().await
}
async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>> {
self.get_descriptor_batch_inner(ids).await
}
async fn get_layer_assignments(&mut self) -> Option<EpochRewardedSet> {
self.get_layer_assignments_inner().await
}
}
// // Test requires running a local instance of the nym-api binary, for example using:
// // `RUST_LOG="info" ./target/debug/nym-api run --nyxd-validator "https://rpc.nymtech.net"`
// #[cfg(test)]
// mod test {
// use std::time::Duration;
// use super::*;
// use nym_bin_common::logging::setup_tracing_logger;
// #[tokio::test]
// async fn local_api_provider_test() {
// setup_tracing_logger();
// let mut provider = NymApiTopologyProvider::new(
// Config::default(),
// vec!["http://localhost:8000"
// .parse()
// .expect("failed to parse api url")],
// None,
// None,
// );
// for _ in 0..180 {
// let topo = provider.get_new_topology().await;
// assert!(topo.is_some());
// tokio::time::sleep(Duration::from_secs(30)).await;
// }
// }
// }
+10 -2
View File
@@ -11,6 +11,8 @@ use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKeyRef;
use nym_validator_client::UserAgent;
use rand::{seq::SliceRandom, Rng};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::{sync::Arc, time::Duration};
use tungstenite::Message;
use url::Url;
@@ -313,9 +315,15 @@ pub(super) async fn register_with_gateway(
gateway_id: identity::PublicKey,
gateway_listener: Url,
our_identity: Arc<identity::KeyPair>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<RegistrationResult, ClientCoreError> {
let mut gateway_client =
GatewayClient::new_init(gateway_listener, gateway_id, our_identity.clone());
let mut gateway_client = GatewayClient::new_init(
gateway_listener,
gateway_id,
our_identity.clone(),
#[cfg(unix)]
connection_fd_callback,
);
gateway_client.establish_connection().await.map_err(|err| {
log::warn!("Failed to establish connection with gateway!");
+22 -4
View File
@@ -23,6 +23,8 @@ use nym_topology::node::RoutingNode;
use rand::rngs::OsRng;
use rand::{CryptoRng, RngCore};
use serde::Serialize;
#[cfg(unix)]
use std::{os::fd::RawFd, sync::Arc};
pub mod helpers;
pub mod types;
@@ -53,6 +55,7 @@ async fn setup_new_gateway<K, D>(
details_store: &D,
selection_specification: GatewaySelectionSpecification,
available_gateways: Vec<RoutingNode>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<InitialisationResult, ClientCoreError>
where
K: KeyStore,
@@ -108,9 +111,14 @@ where
// if we're using a 'normal' gateway setup, do register
let our_identity = client_keys.identity_keypair();
let registration =
helpers::register_with_gateway(gateway_id, gateway_listener.clone(), our_identity)
.await?;
let registration = helpers::register_with_gateway(
gateway_id,
gateway_listener.clone(),
our_identity,
#[cfg(unix)]
connection_fd_callback,
)
.await?;
(
GatewayDetails::new_remote(
gateway_id,
@@ -203,9 +211,19 @@ where
GatewaySetup::New {
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback,
} => {
log::debug!("GatewaySetup::New with spec: {specification:?}");
setup_new_gateway(key_store, details_store, specification, available_gateways).await
setup_new_gateway(
key_store,
details_store,
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback,
)
.await
}
GatewaySetup::ReuseConnection {
authenticated_ephemeral_client,
+10
View File
@@ -18,6 +18,8 @@ use nym_validator_client::client::IdentityKey;
use nym_validator_client::nyxd::AccountId;
use serde::Serialize;
use std::fmt::{Debug, Display};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::sync::Arc;
use time::OffsetDateTime;
use url::Url;
@@ -208,6 +210,10 @@ pub enum GatewaySetup {
// TODO: seems to be a bit inefficient to pass them by value
available_gateways: Vec<RoutingNode>,
/// Callback useful for allowing initial connection to gateway
#[cfg(unix)]
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
},
ReuseConnection {
@@ -231,6 +237,8 @@ impl Debug for GatewaySetup {
GatewaySetup::New {
specification,
available_gateways,
#[cfg(unix)]
connection_fd_callback: _,
} => f
.debug_struct("GatewaySetup::New")
.field("specification", specification)
@@ -270,6 +278,8 @@ impl GatewaySetup {
additional_data: None,
},
available_gateways: vec![],
#[cfg(unix)]
connection_fd_callback: None,
}
}
+1 -2
View File
@@ -14,8 +14,7 @@ pub mod error;
pub mod init;
pub use nym_topology::{
providers::HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError,
TopologyProvider,
HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError, TopologyProvider,
};
#[cfg(target_arch = "wasm32")]
@@ -1065,6 +1065,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
gateway_listener: Url,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Self {
log::trace!("Initialising gateway client");
use futures::channel::mpsc;
@@ -1090,7 +1091,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
stats_reporter: ClientStatsSender::new(None, task_client.clone()),
negotiated_protocol: None,
#[cfg(unix)]
connection_fd_callback: None,
connection_fd_callback,
task_client,
}
}
@@ -38,7 +38,8 @@ pub(crate) async fn connect_async(
// Do a DNS lookup for the domain using our custom DNS resolver
resolver
.resolve_str(domain)
.await?
.await
.inspect_err(|err| tracing::error!("Resolve error {err}"))?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
@@ -49,20 +50,27 @@ pub(crate) async fn connect_async(
address: endpoint.to_owned(),
});
for sock_addr in sock_addrs {
tracing::info!("Trying with {sock_addr}");
let socket = if sock_addr.is_ipv4() {
TcpSocket::new_v4()
} else {
TcpSocket::new_v6()
}
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
.map_err(|err| {
tracing::error!("Couldn't create the socket");
GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
}
})?;
tracing::info!("Preparing to call callback");
#[cfg(unix)]
if let Some(callback) = connection_fd_callback.as_ref() {
tracing::info!("Calling callback");
callback.as_ref()(socket.as_raw_fd());
}
tracing::info!("Preparing to connect");
match socket.connect(sock_addr).await {
Ok(s) => {
@@ -498,21 +498,6 @@ impl NymApiClient {
Ok(nodes)
}
/// Batch request for node descriptors in the current topology.
///
/// Given the set of node IDs included in the request body, provide the descriptor for each
/// associated node in if it is available in the current topology.
pub async fn retrieve_basic_nodes_batch(
&self,
node_ids: &[u32],
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
Ok(self
.nym_api
.retrieve_basic_nodes_batch(node_ids)
.await?
.nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
@@ -270,10 +270,10 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
"mixnodes",
routes::SKIMMED,
"skimmed",
],
NO_PARAMS,
)
@@ -286,10 +286,10 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
"gateways",
routes::SKIMMED,
"skimmed",
],
NO_PARAMS,
)
@@ -335,9 +335,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
"skimmed",
"entry-gateways",
"all",
],
@@ -372,9 +372,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
"skimmed",
"mixnodes",
"active",
],
@@ -409,9 +409,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
"skimmed",
"mixnodes",
"all",
],
@@ -420,31 +420,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
/// Send a Post request with a set of node ids. A successful response will contain descriptors
/// for all nodes associated with those node IDs available in the current full topology.
///
/// If a provided node ID is not present there will be no descriptor for that node in the response.
///
/// If no node IDs are provided the response will contain no descriptors.
#[instrument(level = "debug", skip(self))]
async fn retrieve_basic_nodes_batch(
&self,
node_ids: &[NodeId],
) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
routes::BATCH,
],
NO_PARAMS,
node_ids,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
@@ -469,9 +444,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::UNSTABLE,
"unstable",
routes::NYM_NODES_ROUTES,
routes::SKIMMED,
"skimmed",
],
&params,
)
@@ -74,7 +74,3 @@ pub const SERVICE_PROVIDERS: &str = "services";
pub const DETAILS: &str = "details";
pub const CHAIN_STATUS: &str = "chain-status";
pub const NETWORK: &str = "network";
pub const UNSTABLE: &str = "unstable";
pub const SKIMMED: &str = "skimmed";
pub const BATCH: &str = "batch";
+3 -8
View File
@@ -17,9 +17,6 @@ rand = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["macros", "sync"] }
# 'serde' feature
serde_json = { workspace = true, optional = true }
@@ -38,9 +35,7 @@ nym-sphinx-types = { path = "../nymsphinx/types", features = [
"outfox",
] }
nym-sphinx-routing = { path = "../nymsphinx/routing" }
nym-client-core-config-types = { path = "../client-core/config-types", features = [
"disk-persistence",
] }
# I'm not sure how to feel about pulling in this dependency here...
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
@@ -50,8 +45,8 @@ nym-api-requests = { path = "../../nym-api/nym-api-requests" }
wasm-utils = { path = "../wasm/utils", default-features = false, optional = true }
[features]
default = ["providers"]
providers = ["async-trait"]
default = ["provider-trait"]
provider-trait = ["async-trait"]
wasm-serde-types = ["tsify", "wasm-bindgen", "wasm-utils"]
persistence = ["serde_json"]
outfox = []
+16 -31
View File
@@ -23,13 +23,13 @@ pub mod error;
pub mod node;
pub mod rewarded_set;
#[cfg(feature = "provider-trait")]
pub mod provider_trait;
#[cfg(feature = "wasm-serde-types")]
pub mod wasm_helpers;
#[cfg(feature = "providers")]
pub mod providers;
#[cfg(feature = "providers")]
pub use providers::TopologyProvider;
#[cfg(feature = "provider-trait")]
pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider};
#[deprecated]
#[derive(Debug, Clone)]
@@ -91,6 +91,18 @@ mod deprecated_network_address_impls {
pub type MixLayer = u8;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct NymTopology {
// for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering
// so we use the same 'master' rewarded set information for that
//
// how do we solve the problem of "we have to go through a node that we want to filter out?"
// ¯\_(ツ)_/¯ that's a future problem
rewarded_set: CachedEpochRewardedSet,
node_details: HashMap<NodeId, RoutingNode>,
}
#[derive(Clone, Debug, Default)]
pub struct NymRouteProvider {
pub topology: NymTopology,
@@ -177,18 +189,6 @@ impl NymRouteProvider {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct NymTopology {
// for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering
// so we use the same 'master' rewarded set information for that
//
// how do we solve the problem of "we have to go through a node that we want to filter out?"
// ¯\_(ツ)_/¯ that's a future problem
rewarded_set: CachedEpochRewardedSet,
node_details: HashMap<NodeId, RoutingNode>,
}
impl NymTopology {
pub fn new_empty(rewarded_set: impl Into<CachedEpochRewardedSet>) -> Self {
NymTopology {
@@ -546,19 +546,4 @@ impl NymTopology {
.values()
.filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id))
}
pub fn all_nodes(&self) -> impl Iterator<Item = &RoutingNode> {
self.node_details.values()
}
pub fn all_node_ids(&self) -> impl Iterator<Item = &NodeId> {
self.node_details.keys()
}
pub fn gateways(&self) -> impl Iterator<Item = &RoutingNode> {
self.node_details.values().filter(|n| {
self.rewarded_set.entry_gateways.contains(&n.node_id)
|| self.rewarded_set.exit_gateways.contains(&n.node_id)
})
}
}
+3 -12
View File
@@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
use thiserror::Error;
pub use nym_mixnet_contract_common::reward_params::Performance;
pub use nym_mixnet_contract_common::LegacyMixLayer;
#[derive(Error, Debug)]
@@ -20,7 +19,7 @@ pub enum RoutingNodeError {
NoIpAddressesProvided { node_id: NodeId, identity: String },
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EntryDetails {
// to allow client to choose ipv6 preference, if available
pub ip_addresses: Vec<IpAddr>,
@@ -29,7 +28,7 @@ pub struct EntryDetails {
pub clients_wss_port: Option<u16>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SupportedRoles {
pub mixnode: bool,
pub mixnet_entry: bool,
@@ -46,7 +45,7 @@ impl From<DeclaredRoles> for SupportedRoles {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoutingNode {
pub node_id: NodeId,
@@ -57,7 +56,6 @@ pub struct RoutingNode {
pub sphinx_key: x25519::PublicKey,
pub supported_roles: SupportedRoles,
pub performance: Performance,
}
impl RoutingNode {
@@ -111,12 +109,6 @@ impl<'a> From<&'a RoutingNode> for SphinxNode {
}
}
impl<'a> From<&'a RoutingNode> for RoutingNode {
fn from(node: &'a RoutingNode) -> Self {
node.clone()
}
}
impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode {
type Error = RoutingNodeError;
@@ -146,7 +138,6 @@ impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode {
identity_key: value.ed25519_identity_pubkey,
sphinx_key: value.x25519_sphinx_pubkey,
supported_roles: value.supported_roles.into(),
performance: value.performance,
})
}
}
@@ -4,9 +4,7 @@
use crate::NymTopology;
pub use async_trait::async_trait;
/// Cached Topology Provider built using efficient piecewise requests.
pub mod piecewise;
// hehe, wasm
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait TopologyProvider: Send {
-482
View File
@@ -1,482 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
//!
//!
#![warn(missing_docs)]
use crate::{EpochRewardedSet, NymTopology, RoutingNode, TopologyProvider};
use async_trait::async_trait;
use time::OffsetDateTime;
use tokio::sync::Mutex;
use tracing::{debug, warn};
use std::{cmp::min, collections::HashSet, sync::Arc, time::Duration};
/// Topology filtering and caching configuration
#[derive(Debug)]
pub struct Config {
/// Specifies a minimum performance of a mixnode that is used on route construction.
/// This setting is only applicable when `NymApi` topology is used.
pub min_mixnode_performance: u8,
/// Specifies a minimum performance of a gateway that is used on route construction.
/// This setting is only applicable when `NymApi` topology is used.
pub min_gateway_performance: u8,
/// Specifies whether this client should attempt to retrieve all available network nodes
/// as opposed to just active mixnodes/gateways.
pub use_extended_topology: bool,
/// Specifies whether this client should ignore the current epoch role of the target egress node
/// when constructing the final hop packets.
pub ignore_egress_epoch_role: bool,
/// Minimum duration during which querying the topology will NOT attempt to re-fetch data, and
/// will be served from cache.
pub cache_ttl: Duration,
}
impl Default for Config {
fn default() -> Self {
Self {
min_mixnode_performance: 50,
min_gateway_performance: 0,
use_extended_topology: false,
ignore_egress_epoch_role: true,
cache_ttl: Self::DEFAULT_TOPOLOGY_CACHE_TTL,
}
}
}
impl From<nym_client_core_config_types::Topology> for Config {
fn from(value: nym_client_core_config_types::Topology) -> Self {
Config {
min_mixnode_performance: value.minimum_mixnode_performance,
min_gateway_performance: value.minimum_gateway_performance,
use_extended_topology: value.use_extended_topology,
ignore_egress_epoch_role: value.ignore_egress_epoch_role,
cache_ttl: value.topology_refresh_rate,
}
}
}
impl Config {
/// Default duration during which the topology will be reproduced from cache.
pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(120);
// if we're using 'extended' topology, filter the nodes based on the lowest set performance
fn min_node_performance(&self) -> u8 {
min(self.min_mixnode_performance, self.min_gateway_performance)
}
}
/// Topology Provider build around a cached piecewise provider that uses the Nym API to
/// fetch changes and node details.
#[derive(Clone)]
pub struct NymTopologyProvider<M: PiecewiseTopologyProvider> {
inner: Arc<Mutex<NymTopologyProviderInner<M>>>,
}
impl<M: PiecewiseTopologyProvider> NymTopologyProvider<M> {
/// Construct a new thread safe Cached topology provider
pub fn new(
manager: M,
config: Config,
initial_topology: Option<NymTopology>,
) -> NymTopologyProvider<M> {
let inner = NymTopologyProviderInner::new(config, manager, initial_topology);
NymTopologyProvider {
inner: Arc::new(Mutex::new(inner)),
}
}
/// Bypass the caching for the topology and force a check for the latest updates next time the
/// topology is requested. This fn requires async to get lock in case other threads have access
/// to the cached topology state.
pub async fn force_refresh(&self) {
let mut guard = self.inner.lock().await;
guard.cached_at = OffsetDateTime::UNIX_EPOCH;
}
/// Remove all stored topology state. The next time the topology is requested this will force a
/// pull of all topology information. This fn requires async to get lock in case other threads
/// have access to the cached topology state.
///
/// WARNING: This may be slow / require non-trivial bandwidth.
pub async fn force_clear(&self) {
let mut guard = self.inner.lock().await;
guard.cached = None;
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl<M: PiecewiseTopologyProvider> TopologyProvider for NymTopologyProvider<M> {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let mut guard = self.inner.lock().await;
// check the cache
if let Some(cached) = guard.get_current_compatible_topology().await {
return Some(cached);
}
// not cached, or cache expired. try update.
guard.update_cache().await;
guard.get_current_compatible_topology().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl<M: PiecewiseTopologyProvider> TopologyProvider for NymTopologyProvider<M> {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let mut guard = self.inner.lock().await;
// check the cache
if let Some(cached) = guard.get_current_compatible_topology().await {
return Some(cached);
}
// not cached, or cache expired. try update.
guard.update_cache().await;
guard.get_current_compatible_topology().await
}
}
struct NymTopologyProviderInner<M: PiecewiseTopologyProvider> {
config: Config,
cached: Option<NymTopology>,
cached_at: OffsetDateTime,
topology_manager: M,
}
impl<M: PiecewiseTopologyProvider> NymTopologyProviderInner<M> {
pub fn new(
config: impl Into<Config>,
manager: M,
initial_topology: Option<NymTopology>,
) -> Self {
Self {
config: config.into(),
cached_at: OffsetDateTime::UNIX_EPOCH,
cached: initial_topology,
topology_manager: manager,
}
}
fn cached_topology(&self) -> Option<NymTopology> {
if let Some(cached_topology) = &self.cached {
if self.cached_at + self.config.cache_ttl > OffsetDateTime::now_utc() {
return Some(cached_topology.clone());
}
}
None
}
async fn update_cache(&mut self) {
if let Some(ref mut cached_topology) = self.cached {
// get layer assignment map
let response = self.topology_manager.get_layer_assignments().await;
if response.is_none() {
warn!("pulled layer assignments and got no response");
self.cached_at = OffsetDateTime::now_utc();
return;
}
let layer_assignments = response.unwrap();
// Check if we already know about the epoch
if cached_topology.rewarded_set.epoch_id == layer_assignments.epoch_id {
debug!("pulled layer assignments, epoch already known");
self.cached_at = OffsetDateTime::now_utc();
return;
}
cached_topology.rewarded_set = layer_assignments.into();
// get the set of node IDs
let new_id_set = cached_topology.rewarded_set.all_ids();
let known_id_set = HashSet::<u32>::from_iter(cached_topology.all_node_ids().copied());
let unknown_node_ids: Vec<_> = new_id_set.difference(&known_id_set).copied().collect();
// Pull node descriptors for unknown IDs
let response = self
.topology_manager
.get_descriptor_batch(&unknown_node_ids[..])
.await;
// Add the new nodes to our cached topology
if let Some(new_descriptors) = response {
cached_topology.add_routing_nodes(new_descriptors);
}
// double check that we have the expected nodes
let known_id_set = HashSet::<u32>::from_iter(cached_topology.all_node_ids().copied());
let unknown_node_ids: Vec<_> = new_id_set.difference(&known_id_set).collect();
if !unknown_node_ids.is_empty() {
warn!(
"still missing descriptors for nodes in the assigned set: {:?}",
unknown_node_ids
);
}
} else {
self.cached = self.topology_manager.get_full_topology().await;
}
self.cached_at = OffsetDateTime::now_utc();
}
/// Gets the current topology state using `Self::cached_topology` and then applies any filters
/// defined in the provided Config.
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let full_topology = self.cached_topology()?;
let mut topology = NymTopology::new_empty(full_topology.rewarded_set().clone());
if self.config.use_extended_topology {
topology.add_additional_nodes(full_topology.all_nodes().filter(|n| {
n.performance.round_to_integer() >= self.config.min_node_performance()
}));
return Some(full_topology);
}
topology.add_additional_nodes(
full_topology.mixnodes().filter(|m| {
m.performance.round_to_integer() >= self.config.min_mixnode_performance
}),
);
topology.add_additional_nodes(
full_topology.gateways().filter(|m| {
m.performance.round_to_integer() >= self.config.min_gateway_performance
}),
);
Some(topology)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl<P: PiecewiseTopologyProvider> TopologyProvider for NymTopologyProviderInner<P> {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
impl<P: PiecewiseTopologyProvider> TopologyProvider for NymTopologyProviderInner<P> {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
self.get_current_compatible_topology().await
}
}
#[cfg(not(target_arch = "wasm32"))]
/// Trait allowing construction and upkeep of a
#[async_trait]
pub trait PiecewiseTopologyProvider: Send {
/// Pull a copy of the full topology.
///
/// This is intended to be used sparingly as repeated usage could result in fetching duplicate
/// information more often than necessary.
async fn get_full_topology(&mut self) -> Option<NymTopology>;
/// Fetch a node descriptors for the set of provided IDs if available.
async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>>;
/// Fetch the latest mapping of node IDs to Nym Network layer.
async fn get_layer_assignments(&mut self) -> Option<EpochRewardedSet>;
}
#[cfg(target_arch = "wasm32")]
/// Trait allowing construction and upkeep of a
#[async_trait(?Send)]
pub trait PiecewiseTopologyProvider: Send {
/// Pull a copy of the full topology.
///
/// This is intended to be used sparingly as repeated usage could result in fetching duplicate
/// information more often than necessary.
async fn get_full_topology(&mut self) -> Option<NymTopology>;
/// Fetch a node descriptors for the set of provided IDs if available.
async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>>;
/// Fetch the latest mapping of node IDs to Nym Network layer.
async fn get_layer_assignments(&mut self) -> Option<EpochRewardedSet>;
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod test {
use super::*;
use crate::SupportedRoles;
use nym_crypto::asymmetric::encryption::PublicKey as SphinxPubkey;
use nym_crypto::asymmetric::identity::PublicKey as IdentityPubkey;
use nym_mixnet_contract_common::Percent;
#[derive(Clone)]
struct PassthroughPiecewiseTopologyProvider {
topo: NymTopology,
}
#[async_trait]
impl PiecewiseTopologyProvider for PassthroughPiecewiseTopologyProvider {
async fn get_full_topology(&mut self) -> Option<NymTopology> {
Some(self.topo.clone())
}
async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option<Vec<RoutingNode>> {
let mut nodes = Vec::new();
ids.iter().for_each(|id| {
if let Some(node) = self.topo.node_details.get(id) {
nodes.push(node.clone());
}
});
Some(nodes)
}
async fn get_layer_assignments(&mut self) -> Option<EpochRewardedSet> {
return Some(self.topo.rewarded_set.clone().into());
}
}
#[tokio::test]
async fn test_topology_provider() -> Result<(), Box<dyn std::error::Error>> {
let mut topo_mgr = PassthroughPiecewiseTopologyProvider {
topo: NymTopology::default(),
};
let mut topo_provider =
NymTopologyProviderInner::new(Config::default(), topo_mgr.clone(), None);
// No initial topology was provided, No update has run yet, None should be returned
assert_eq!(topo_provider.cached_topology(), None);
// force an update of the cached topology
topo_provider.update_cache().await;
let topo = topo_provider.cached_topology();
assert!(topo.is_some());
let topo = topo.unwrap();
assert!(topo.is_empty());
// create a change in the manager to make sure it is propogated to the provider cache on update
topo_mgr.topo.rewarded_set.epoch_id += 1;
topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123]);
assert_eq!(topo_mgr.topo.node_details.insert(123, fake_node(123)), None);
topo_provider.topology_manager = topo_mgr.clone();
// force an update of the cached topology
topo_provider.update_cache().await;
let topo = topo_provider.cached_topology();
assert!(topo.is_some());
let topo1 = topo.unwrap();
assert!(!topo1.is_empty());
assert!(topo1.node_details.contains_key(&123));
// try forcing an update even though the epoch has not changed. Should result in no change
topo_provider.update_cache().await;
let topo2 = topo_provider.cached_topology().unwrap();
assert_eq!(topo1, topo2);
// Add a node without a descriptor to make sure warning is printed.
topo_mgr.topo.rewarded_set.epoch_id += 1;
topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123, 456]);
topo_provider.topology_manager = topo_mgr.clone();
// try forcing an update even though the epoch has not changed. Should result in no change
topo_provider.update_cache().await;
let _ = topo_provider.cached_topology().unwrap();
Ok(())
}
#[tokio::test]
async fn test_topology_provider_by_trait() -> Result<(), Box<dyn std::error::Error>> {
let mut topo_mgr = PassthroughPiecewiseTopologyProvider {
topo: NymTopology::default(),
};
let mut topo_provider = NymTopologyProvider::new(topo_mgr.clone(), Config::default(), None);
// No initial topology was provided, the NymTopologyProvider should do an update from the
// manager to build its cache. This should be our empty topology initialized in the manage
// above
let maybe_topo = topo_provider.get_new_topology().await;
assert!(maybe_topo.is_some());
let topo1 = maybe_topo.unwrap();
assert!(topo1.is_empty());
// Try pulling again, should give response from cache because we are under ttl
let maybe_topo = topo_provider.get_new_topology().await;
assert!(maybe_topo.is_some());
let topo2 = maybe_topo.unwrap();
assert_eq!(topo1, topo2);
// create a change in the manager
topo_mgr.topo.rewarded_set.epoch_id += 1;
topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123]);
assert_eq!(topo_mgr.topo.node_details.insert(123, fake_node(123)), None);
{
let mut guard = topo_provider.inner.lock().await;
guard.topology_manager = topo_mgr.clone();
drop(guard)
}
// The NymTopologyProvider should still serve from cache because we haven't crossed ttl
// despite updates being available in the manager
let maybe_topo = topo_provider.get_new_topology().await;
assert!(maybe_topo.is_some());
let topo3 = maybe_topo.unwrap();
assert_eq!(topo2, topo3);
// force ttl timeout should allow refresh that includes latest changes from manager
topo_provider.force_refresh().await;
let maybe_topo = topo_provider.get_new_topology().await;
assert!(maybe_topo.is_some());
let topo4 = maybe_topo.unwrap();
assert_ne!(topo3, topo4);
assert!(topo4.node_details.contains_key(&123));
// create another change in the manager
topo_mgr.topo.rewarded_set.epoch_id += 1;
topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123, 456]);
assert_eq!(topo_mgr.topo.node_details.insert(456, fake_node(456)), None);
{
let mut guard = topo_provider.inner.lock().await;
guard.topology_manager = topo_mgr.clone();
drop(guard)
}
// force clear cache should also pull latest full topology
topo_provider.force_clear().await;
let maybe_topo = topo_provider.get_new_topology().await;
assert!(maybe_topo.is_some());
let topo5 = maybe_topo.unwrap();
assert!(topo5.node_details.contains_key(&456));
Ok(())
}
fn fake_node(node_id: u32) -> RoutingNode {
RoutingNode {
node_id,
mix_host: "127.0.0.1:2345".parse().unwrap(),
entry: None,
identity_key: IdentityPubkey::from_bytes(&[0u8; 32][..]).unwrap(),
sphinx_key: SphinxPubkey::from_bytes(&[0u8; 32][..]).unwrap(),
supported_roles: SupportedRoles {
mixnode: true,
mixnet_entry: true,
mixnet_exit: true,
},
performance: Percent::hundred(),
}
}
}
+1 -19
View File
@@ -6,7 +6,7 @@ use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, NodeId, RewardedSet}
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CachedEpochRewardedSet {
pub epoch_id: EpochId,
@@ -119,22 +119,4 @@ impl CachedEpochRewardedSet {
mixnodes.extend(&self.layer3);
mixnodes
}
pub fn all_ids(&self) -> HashSet<NodeId> {
let mut mixnodes = HashSet::with_capacity(
self.entry_gateways.len()
+ self.exit_gateways.len()
+ self.layer1.len()
+ self.layer2.len()
+ self.layer3.len()
+ self.standby.len(),
);
mixnodes.extend(&self.entry_gateways);
mixnodes.extend(&self.exit_gateways);
mixnodes.extend(&self.layer1);
mixnodes.extend(&self.layer2);
mixnodes.extend(&self.layer3);
mixnodes.extend(&self.standby);
mixnodes
}
}
-3
View File
@@ -6,7 +6,6 @@
use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles};
use crate::{CachedEpochRewardedSet, NymTopology};
use nym_mixnet_contract_common::reward_params::Performance;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
@@ -106,8 +105,6 @@ impl TryFrom<WasmFriendlyRoutingNode> for RoutingNode {
}
})?,
supported_roles: value.supported_roles,
performance: Performance::hundred(),
})
}
}
+1 -3
View File
@@ -28,9 +28,7 @@ pub use nym_sphinx::{
};
pub use nym_statistics_common::clients::ClientStatsSender;
pub use nym_task;
pub use nym_topology::{
providers::HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider,
};
pub use nym_topology::{HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider};
pub use nym_validator_client::nym_api::Client as ApiClient;
pub use nym_validator_client::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient};
// TODO: that's a very nasty import path. it should come from contracts instead!
-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 committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-36
View File
@@ -1,36 +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
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/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.
-33
View File
@@ -1,33 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["node_modules", ".next", "public"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
-23
View File
@@ -1,23 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
basePath: "/explorer",
assetPrefix: "/explorer",
trailingSlash: false,
async redirects() {
return [
// Change the basePath to /explorer
{
source: "/",
destination: "/explorer",
basePath: false,
permanent: true,
},
]
}
};
export default nextConfig;
-64
View File
@@ -1,64 +0,0 @@
{
"name": "@nymproject/explorer-v2",
"version": "0.1.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"build:prod": "yarn --cwd .. build && next build",
"start": "next start",
"lint": "biome check --fix"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@chain-registry/types": "^0.50.36",
"@cosmos-kit/keplr-extension": "^2.14.0",
"@cosmos-kit/react": "^2.20.1",
"@emotion/cache": "^11.13.5",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5",
"@interchain-ui/react": "^1.26.1",
"@mui/icons-material": "^5.16.11",
"@mui/material": "^6.1.10",
"@mui/material-nextjs": "^6.1.9",
"@mui/x-date-pickers": "^7.23.2",
"@nivo/line": "^0.88.0",
"@nymproject/contract-clients": "^1.4.1",
"@nymproject/react": "1.0.0",
"@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2",
"@tanstack/react-query-next-experimental": "^5.66.0",
"@tanstack/react-table": "^8.20.6",
"@types/qs": "^6.9.18",
"@uidotdev/usehooks": "^2.4.1",
"chain-registry": "^1.69.64",
"cldr-compact-number": "^0.4.0",
"date-fns": "^4.1.0",
"i18next": "^24.2.2",
"i18next-resources-to-backend": "^1.2.1",
"isomorphic-dompurify": "^2.21.0",
"material-react-table": "^3.0.3",
"next": "^15.2.0",
"openapi-fetch": "^0.13.4",
"qrcode.react": "^4.1.0",
"qs": "^6.14.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.3",
"react-random-avatars": "^1.3.1",
"react-world-flags": "^1.6.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"lefthook": "^1.8.5",
"typescript": "^5"
}
}
-1
View File
@@ -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
View File
@@ -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,61 +0,0 @@
<svg
width="33"
height="32"
viewBox="0 0 33 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 7.06641 25.5204)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 0.666504 31.9006)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 13.4663 19.1406)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 19.8667 12.761)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M32.6663 32L26.2663 32L26.2663 6.38018L0.708989 6.38017L0.70899 -1.52588e-05L24.9863 -1.1014e-05C29.2278 -1.02724e-05 32.6663 3.43845 32.6663 7.68L32.6663 32Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="15" width="3" height="3" transform="rotate(-90 0 15)" fill="currentColor" />
<rect x="3" y="12" width="3" height="3" transform="rotate(-90 3 12)" fill="currentColor" />
<rect x="6" y="9" width="3" height="3" transform="rotate(-90 6 9)" fill="currentColor" />
<rect x="9" y="6" width="3" height="3" transform="rotate(-90 9 6)" fill="currentColor"/>
<path d="M0 2.19345e-05V3.00002H11.9997V15H14.9997V3.00002C14.9997 1.34317 13.6565 2.19345e-05 11.9997 2.19345e-05H0Z" fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 579 B

-31
View File
@@ -1,31 +0,0 @@
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_236_24563)">
<path d="M180.789 120.41V150.41H210.789V180.41H240.789V180.41C240.789 147.273 213.926 120.41 180.789 120.41V120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M180.789 240.487L180.789 210.487L150.789 210.487L150.789 180.487L120.789 180.487V180.487C120.789 213.624 147.652 240.487 180.789 240.487V240.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M120.789 180.41L150.789 180.41L150.789 150.41L180.789 150.41L180.789 120.41V120.41C147.652 120.41 120.789 147.273 120.789 180.41V180.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M240.789 180.487L210.789 180.487L210.789 210.487L180.789 210.487L180.789 240.487V240.487C213.926 240.487 240.789 213.624 240.789 180.487V180.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M330.773 120.41L330.773 150.41V150.41C347.342 150.41 360.773 136.979 360.773 120.41V120.41L330.773 120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M270.789 60.4102L270.789 90.4102L300.789 90.4102L300.789 60.4102L270.789 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M240.789 30.4102L240.789 60.4102L270.789 60.4102L270.789 30.4102L240.789 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M60.7891 30.4102L60.7891 60.4102L90.7891 60.4102L90.7891 30.4102L60.7891 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M30.7734 60.4102L30.7734 90.4102L60.7734 90.4102L60.7734 60.4102L30.7734 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M90.7891 0.410156L90.7891 30.4102L240.789 30.4102L240.789 0.410181L90.7891 0.410156Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="30" height="150.466" transform="matrix(-1.62921e-07 1 1 1.62921e-07 180.789 120.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="120" height="30" transform="matrix(-1.62921e-07 1 1 1.62921e-07 330.773 0.410156)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M301.023 90.4102L301.023 120.41L331.023 120.41L331.023 90.4102L301.023 90.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M30.7734 240.41L30.7734 210.41V210.41C14.2049 210.41 0.77344 223.842 0.773438 240.41V240.41L30.7734 240.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M90.7734 300.41L90.7734 270.41L60.7734 270.41L60.7734 300.41L90.7734 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M120.773 330.41L120.773 300.41L90.7734 300.41L90.7734 330.41L120.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M300.773 330.41L300.773 300.41L270.773 300.41L270.773 330.41L300.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M330.773 300.41L330.773 270.41L300.773 270.41L300.773 300.41L330.773 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M270.773 360.41L270.773 330.41L120.773 330.41L120.773 360.41L270.773 360.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="30" height="150.481" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 180.789 240.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="120" height="30" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 30.7734 360.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M60.5391 270.41L60.5391 240.41L30.5391 240.41L30.5391 270.41L60.5391 270.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</g>
<defs>
<clipPath id="clip0_236_24563">
<rect width="360" height="360" fill="white" style="fill:white;fill-opacity:1;" transform="translate(0.773438 0.410156)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

-13
View File
@@ -1,13 +0,0 @@
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3L-4.47035e-08 3L-8.31509e-07 6L3 6L3 3Z" fill="currentColor" />
<path d="M6 6L3 6L3 9L6 9L6 6Z" fill="currentColor" />
<path d="M9 9L6 9L6 12L9 12L9 9Z" fill="currentColor" />
<path d="M12 6L9 6L9 9L12 9L12 6Z" fill="currentColor" />
<path d="M15 3L12 3L12 6L15 6L15 3Z" fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 431 B

-14
View File
@@ -1,14 +0,0 @@
<svg
width="9"
height="5"
viewBox="0 0 9 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.828125 0.828125L4.5 4.5L8.17188 0.828125"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

-35
View File
@@ -1,35 +0,0 @@
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.6668 13.3333V14.6667H10.0002V15.3333H2.00016V14.6667H1.3335V3.99999H2.00016V3.33333H4.00016V13.3333H10.6668Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M10.6665 4.66667V0.666672H5.33317V1.33334H4.6665V12H5.33317V12.6667H13.9998V12H14.6665V4.66667H10.6665ZM13.3332 11.3333H5.99984V2.00001H9.33317V6.00001H13.3332V11.3333Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M14.6668 3.33334V4H11.3335V0.666672H12.0002V1.33334H12.6668V2.00001H13.3335V2.66667H14.0002V3.33334H14.6668Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

Before

Width:  |  Height:  |  Size: 967 B

-135
View File
@@ -1,135 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
>
<g clip-path="url(#clip0_8428_65000)">
<rect
y="3"
width="3"
height="3"
transform="rotate(-90 0 3)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
y="15"
width="3"
height="3"
transform="rotate(-90 0 15)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="12"
y="3"
width="3"
height="3"
transform="rotate(-90 12 3)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="9"
y="12"
width="3"
height="3"
transform="rotate(-90 9 12)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="12"
y="15"
width="3"
height="3"
transform="rotate(-90 12 15)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="3"
y="6"
width="3"
height="3"
transform="rotate(-90 3 6)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="3"
y="12"
width="3"
height="3"
transform="rotate(-90 3 12)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="9"
y="6"
width="3"
height="3"
transform="rotate(-90 9 6)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="6"
y="9"
width="3"
height="3"
transform="rotate(-90 6 9)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</g>
<defs>
<clipPath id="clip0_8428_65000">
<rect
width="15"
height="15"
fill="white"
style="fill: white; fill-opacity: 1"
/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8343 11.2552C23.7199 14.0472 24.6511 17.1964 24.303 20.8219C24.3015 20.8372 24.2936 20.8513 24.2811 20.8606C22.8532 21.9165 21.4697 22.5573 20.1057 22.9823C20.0951 22.9855 20.0837 22.9853 20.0732 22.9817C20.0627 22.9782 20.0535 22.9714 20.047 22.9623C19.7319 22.5207 19.4456 22.0552 19.1947 21.5663C19.1803 21.5375 19.1935 21.5028 19.2231 21.4915C19.6779 21.3189 20.1103 21.1121 20.5262 20.8673C20.559 20.8479 20.5611 20.8007 20.5308 20.778C20.4425 20.712 20.3551 20.6426 20.2714 20.5733C20.2557 20.5604 20.2347 20.5579 20.2169 20.5665C17.5166 21.8223 14.5586 21.8223 11.8263 20.5665C11.8086 20.5585 11.7875 20.5613 11.7723 20.5739C11.6888 20.6432 11.6011 20.712 11.5137 20.778C11.4834 20.8007 11.4859 20.8479 11.5189 20.8673C11.9349 21.1075 12.3673 21.3189 12.8214 21.4923C12.8508 21.5037 12.8648 21.5375 12.8502 21.5663C12.6048 22.0558 12.3184 22.5213 11.9975 22.9629C11.9835 22.9808 11.9605 22.989 11.9388 22.9823C10.5813 22.5573 9.19781 21.9165 7.76992 20.8606C7.75802 20.8513 7.74947 20.8366 7.74822 20.8213C7.45729 17.6853 8.0502 14.51 10.2146 11.2546C10.2198 11.246 10.2277 11.2393 10.2369 11.2353C11.3019 10.743 12.4428 10.3809 13.6353 10.1741C13.657 10.1707 13.6787 10.1808 13.69 10.2002C13.8373 10.4629 14.0057 10.7998 14.1197 11.0751C15.3767 10.8818 16.6532 10.8818 17.9365 11.0751C18.0505 10.8057 18.213 10.4629 18.3597 10.2002C18.365 10.1906 18.3731 10.1829 18.3829 10.1782C18.3927 10.1735 18.4037 10.1721 18.4144 10.1741C19.6075 10.3815 20.7485 10.7437 21.8126 11.2353C21.822 11.2393 21.8297 11.246 21.8343 11.2552V11.2552ZM14.7587 17.2178C14.7719 16.2908 14.1007 15.5236 13.2582 15.5236C12.4226 15.5236 11.7579 16.284 11.7579 17.2178C11.7579 18.1514 12.4357 18.9118 13.2582 18.9118C14.094 18.9118 14.7587 18.1514 14.7587 17.2178V17.2178ZM20.3062 17.2178C20.3194 16.2908 19.6482 15.5236 18.8059 15.5236C17.9701 15.5236 17.3054 16.284 17.3054 17.2178C17.3054 18.1514 17.9833 18.9118 18.8059 18.9118C19.6482 18.9118 20.3062 18.1514 20.3062 17.2178V17.2178Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-10
View File
@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<g clip-path="url(#clip0_6568_5882)">
<path d="M4 8H8V9H4V8ZM4 6H8V7H4V6ZM7 1H3C2.45 1 2 1.45 2 2V10C2 10.55 2.445 11 2.995 11H9C9.55 11 10 10.55 10 10V4L7 1ZM9 10H3V2H6.5V4.5H9V10Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
</g>
<defs>
<clipPath id="clip0_6568_5882">
<rect width="12" height="12" fill="white" style="fill:white;fill-opacity:1;"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 5H2V4H3V4.5H3.5V5H4V5.5H4.5V6H5V6.5H5.5V0.5H6.5V6.5H7V6H7.5V5.5H8V5H8.5V4.5H9V4H10V5H9.5V5.5H9V6H8.5V6.5H8V7H7.5V7.5H7V8H6.5V8.5H5.5V8H5V7.5H4.5V7H4V6.5H3.5V6H3V5.5H2.5V5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
<path d="M11 10.5H1V11.5H11V10.5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
</svg>

Before

Width:  |  Height:  |  Size: 520 B

-22
View File
@@ -1,22 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
>
<circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" />
<defs>
<radialGradient
id="paint0_angular_2549_7570"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12 12.5) rotate(90) scale(12)"
>
<stop stopColor="#22D27E" />
<stop offset="1" stopColor="#9002FF" />
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 515 B

@@ -1,3 +0,0 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0L56 -2.44784e-06L56 7L63 7L70 7H77V14V21L77 28L84 28V21V14C84 6.26801 77.732 0 70 0H63ZM7 14L7 7H14H21L28 7L28 2.14186e-06L21 0H14C6.26801 0 0 6.26801 0 14V21L-3.87835e-06 28L7 28L7 21V14ZM84 70C84 77.732 77.732 84 70 84H63H56L56 77H63H70H77V70L77 63L77 56L84 56V63V70ZM21 77H14L14 70L21 70L21 63H28L28 56H35L35 63H42H49L49 56H56L56 49L63 49L63 42V35L56 35V28L49 28L49 21L42 21H35L35 28L28 28L28 35H21L21 42V49L28 49L28 56H21L21 63L14 63L14 70H7L7 77L7 70L7 63L7 56L0 56L-3.87835e-06 63L0 70C0 77.732 6.26801 84 14 84H21H28L28 77H21ZM28 49L28 42L28 35H35L35 28H42L49 28L49 35L56 35V42L56 49H49V56L42 56H35V49H28Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

-79
View File
@@ -1,79 +0,0 @@
<svg
width="84"
height="84"
viewBox="0 0 84 84"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="7"
height="56"
transform="matrix(-2.36041e-07 1 1 7.28523e-08 28 77)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="56"
transform="matrix(1 0 -1.63189e-07 -1 0 56)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M63 0L63 21L84 21L84 28L63 28C59.134 28 56 24.866 56 21L56 0H63Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="42"
transform="matrix(1 0 -1.63189e-07 -1 28 42)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="35"
transform="matrix(-2.36041e-07 1 1 7.28523e-08 49 49)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M28 84L28 77L7 77L7 56L-1.89145e-06 56L-2.80939e-06 77C-2.97838e-06 80.866 3.134 84 7 84L28 84Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M56 56L56 49L35 49L35 28L28 28L28 49C28 52.866 31.134 56 35 56L56 56Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9408 7.86426C10.9716 7.86426 6.94678 11.8731 6.94678 16.8225C6.94678 20.7866 9.52132 24.1347 13.0965 25.3217C13.5462 25.4001 13.7148 25.1313 13.7148 24.8962C13.7148 24.6834 13.7035 23.9779 13.7035 23.2277C11.4438 23.642 10.8592 22.679 10.6793 22.1751C10.5781 21.9175 10.1397 21.1225 9.75741 20.9097C9.44262 20.7418 8.99292 20.3274 9.74617 20.3162C10.4545 20.3051 10.9604 20.9657 11.129 21.2345C11.9385 22.5894 13.2314 22.2087 13.7485 21.9735C13.8272 21.3912 14.0633 20.9993 14.3219 20.7754C12.3207 20.5514 10.2296 19.7788 10.2296 16.3522C10.2296 15.378 10.5781 14.5718 11.1515 13.9447C11.0616 13.7207 10.7468 12.8025 11.2414 11.5707C11.2414 11.5707 11.9947 11.3356 13.7148 12.489C14.4343 12.2874 15.1988 12.1866 15.9633 12.1866C16.7278 12.1866 17.4923 12.2874 18.2118 12.489C19.9319 11.3244 20.6852 11.5707 20.6852 11.5707C21.1798 12.8025 20.8651 13.7207 20.7751 13.9447C21.3485 14.5718 21.697 15.3668 21.697 16.3522C21.697 19.79 19.5946 20.5514 17.5935 20.7754C17.9195 21.0553 18.2006 21.5928 18.2006 22.4326C18.2006 23.6308 18.1893 24.5938 18.1893 24.8962C18.1893 25.1313 18.358 25.4113 18.8077 25.3217C22.3603 24.1347 24.9349 20.7754 24.9349 16.8225C24.9349 11.8731 20.91 7.86426 15.9408 7.86426Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-14
View File
@@ -1,14 +0,0 @@
<svg viewBox="0 0 89 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 2.73999C0 1.50288 1.00579 0.5 2.2465 0.5H10.4256C11.8792 0.5 13.1513 1.47412 13.5263 2.87439L18.7506 22.3815C18.8474 22.7432 19.3816 22.6733 19.3816 22.299V0.5H25.8001V22.2599C25.8001 23.497 24.7943 24.4999 23.5536 24.4999H15.314C13.8643 24.4999 12.5946 23.5308 12.216 22.1355L7.04928 3.0892C6.95141 2.72844 6.41855 2.79902 6.41855 3.17275V24.4999H0V2.73999Z"
fill="currentColor"
/>
<path
d="M55.5159 0.500025C54.2752 0.500025 53.2694 1.5029 53.2694 2.74002V24.4999H59.6879V3.18233C59.6879 2.79548 60.2483 2.74066 60.3237 3.12013L64.0601 21.9219C64.3579 23.4203 65.6762 24.4999 67.2082 24.4999L74.6942 24.4999C76.2294 24.4999 77.5496 23.4158 77.8439 21.9135L81.5171 3.16662C81.5916 2.78643 82.153 2.84061 82.153 3.22798V24.4999H88.5714V2.74002C88.5714 1.5029 87.5656 0.500025 86.3249 0.500025L78.3813 0.50002C76.859 0.500019 75.5461 1.56646 75.2383 3.05306L71.2361 22.3844C71.1655 22.7251 70.6773 22.7246 70.6074 22.3838L66.6405 3.05837C66.3349 1.56926 65.0208 0.500019 63.4964 0.50002L55.5159 0.500025Z"
fill="currentColor"
/>
<path
d="M26.5569 0.50009H33.3531L39.2253 13.0169C39.4563 13.5093 40.1589 13.5085 40.3888 13.0155L46.2237 0.50009H53.1458L41.8949 24.5H34.9858L39.4713 14.9H35.3564C34.1107 14.9 32.9775 14.1813 32.4496 13.0563L26.5569 0.50009Z"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,3 +0,0 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0H56V7H63H70H77V14V21V28H84V21V14C84 6.26801 77.732 0 70 0H63ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7ZM63 21L63 28L56 28L56 21L63 21ZM63 21V14L70 14L70 21L63 21ZM49 35V28L56 28V35H49ZM49 49V42L49 35L42 35L35 35L35 28L28 28L28 21L21 21L21 14L14 14L14 21L21 21L21 28L28 28V35L35 35V42V49L28 49L28 56L21 56L21 63H14L14 70H21L21 63L28 63L28 56H35V49H42L49 49ZM56 56H49L49 49L56 49V56ZM63 63L56 63V56L63 56L63 63ZM63 63H70V70H63L63 63Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

Before

Width:  |  Height:  |  Size: 872 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M84 14C84 6.26801 77.732 0 70 0H63H56V7H63H70H77V14V21V28H84V21V14ZM28 14C20.268 14 14 20.268 14 28C14 35.732 20.268 42 28 42C20.268 42 14 48.268 14 56C14 63.732 20.268 70 28 70C35.7299 70 41.9966 63.7354 42 56.0062C42.0034 63.7353 48.2701 70 56 70C63.732 70 70 63.732 70 56C70 48.2701 63.7353 42.0034 56.0062 42C63.7353 41.9966 70 35.7299 70 28C70 20.268 63.732 14 56 14C48.268 14 42 20.268 42 28C42 20.268 35.732 14 28 14ZM49 28V35L56 35H63V28V21L56 21H49V28ZM42 28.0062C41.9966 35.7329 35.7339 41.996 28.0074 42C35.736 42.004 42 48.2705 42 56C42 48.268 48.268 42 56 42C48.2701 42 42.0034 35.7353 42 28.0062ZM56 63H63V56V49L56 49H49V56V63L56 63ZM28 35H35V28V21L28 21H21V28L21 35L28 35ZM21 49V56L21 63L28 63H35V56V49L28 49H21ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.90246 15.9332C8.90246 15.9332 15.4128 13.1912 17.6706 12.2256C18.5362 11.8394 21.4715 10.6036 21.4715 10.6036C21.4715 10.6036 22.8262 10.0629 22.7133 11.376C22.6757 11.9167 22.3746 13.8091 22.0736 15.8559C21.622 18.7525 21.1328 21.9194 21.1328 21.9194C21.1328 21.9194 21.0575 22.8077 20.4178 22.9621C19.7781 23.1166 18.7243 22.4215 18.5362 22.267C18.3856 22.1511 15.7138 20.4132 14.7354 19.5635C14.4719 19.3318 14.1709 18.8684 14.773 18.3277C16.1278 17.0532 17.7459 15.4698 18.7243 14.4656C19.1759 14.0022 19.6275 12.9208 17.7459 14.2339C15.0741 16.1263 12.4398 17.9029 12.4398 17.9029C12.4398 17.9029 11.8377 18.289 10.7088 17.9414C9.57979 17.5939 8.26268 17.1304 8.26268 17.1304C8.26268 17.1304 7.35957 16.5511 8.90246 15.9332Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 845 B

-23
View File
@@ -1,23 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
>
<g clipPath="url(#clip0_2549_7563)">
<path
d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z"
fill="black"
/>
<path
d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_2549_7563">
<rect width="24" height="24" fill="white" transform="translate(0 0.5)" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4206 15.4955L22.9798 9.03333H21.6625L16.8354 14.6444L12.98 9.03333H8.5332L14.3633 17.5182L8.5332 24.2948H9.85065L14.9482 18.3694L19.0198 24.2948H23.4665L17.4202 15.4955H17.4206ZM15.6161 17.593L15.0254 16.7481L10.3253 10.0251H12.3489L16.1419 15.4507L16.7326 16.2956L21.6631 23.3482H19.6396L15.6161 17.5933V17.593Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 453 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 810 B

@@ -1,4 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="#242B2D"/>
</svg>

Before

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

-1
View File
@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

-1
View File
@@ -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
View File
@@ -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

@@ -1,62 +0,0 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
import { Box, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import Markdown from "react-markdown";
export default async function Account({
params,
}: {
params: Promise<{ address: string }>;
}) {
try {
const address = (await params).address;
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<SectionHeading title="Nym Node Details" />
<ExplorerButtonGroup
onPage="Account"
options={[
{
label: "Nym Node",
isSelected: true,
link: `/account/${address}/not-found/`,
},
{
label: "Account",
isSelected: false,
link: `/account/${address}`,
},
]}
/>
</Box>
</Grid>
</Grid>
<Typography variant="h5">
<Markdown className="reactMarkDownLink">
This account doest have a Nym node bonded. Is this your account?
Start [setting up your node](https://nym.com/docs) today!
</Markdown>
</Typography>
{/* <Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1]} />
</Grid> */}
</ContentLayout>
);
} catch (error) {
let errorMessage = "An error occurred";
if (error instanceof Error) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
}
@@ -1,67 +0,0 @@
import { fetchNodes } from "@/app/api";
import type { NodeData } from "@/app/api/types";
import { Box, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { AccountBalancesCard } from "../../../../components/accountPageComponents/AccountBalancesCard";
import { AccountInfoCard } from "../../../../components/accountPageComponents/AccountInfoCard";
import { ContentLayout } from "../../../../components/contentLayout/ContentLayout";
import SectionHeading from "../../../../components/headings/SectionHeading";
import ExplorerButtonGroup from "../../../../components/toggleButton/ToggleButton";
export default async function Account({
params,
}: {
params: Promise<{ address: string }>;
}) {
try {
const address = (await params).address;
const nymNodes: NodeData[] = await fetchNodes();
const nymNode = nymNodes.find(
(node) => node.bond_information.owner === address,
);
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={6}>
<SectionHeading title="Account Details" />
</Grid>
<Grid size={6} justifyContent="flex-end">
<Box sx={{ display: "flex", justifyContent: "end" }}>
<ExplorerButtonGroup
onPage="Account"
options={[
{
label: "Nym Node",
isSelected: false,
link: nymNode
? `/nym-node/${nymNode.node_id}`
: `/account/${address}/not-found`,
},
{
label: "Account",
isSelected: true,
link: `/account/${address}`,
},
]}
/>
</Box>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<AccountInfoCard address={address} />
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<AccountBalancesCard address={address} />
</Grid>
</Grid>
</ContentLayout>
);
} catch (error) {
console.error("error :>> ", error);
return <Typography>Error loading account data</Typography>;
}
}
@@ -1,128 +0,0 @@
import { fetchNodeIdByIdentityKey, fetchNodeInfo } from "@/app/api";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
import { NodeDataCard } from "@/components/nymNodePageComponents/NodeDataCard";
// import { NodeChatCard } from "@/components/nymNodePageComponents/ChatCard";
import NodeDelegationsCard from "@/components/nymNodePageComponents/NodeDelegationsCard";
import { NodeParametersCard } from "@/components/nymNodePageComponents/NodeParametersCard";
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
import { NodeRoleCard } from "@/components/nymNodePageComponents/NodeRoleCard";
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid2";
export default async function NymNode({
params,
}: {
params: Promise<{ id: string }>; // node_id or identity_key
}) {
try {
let id: string | number;
const paramsId = (await params).id;
// check if the params id is a node_id or identity_key
if (paramsId.length > 10) {
id = await fetchNodeIdByIdentityKey(paramsId);
} else {
id = Number(paramsId);
}
const observatoryNymNode = await fetchNodeInfo(id);
if (!observatoryNymNode) {
return null;
}
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<SectionHeading title="Nym Node Details" />
{observatoryNymNode.bonding_address && (
<ExplorerButtonGroup
onPage="Nym Node"
options={[
{
label: "Nym Node",
isSelected: true,
link: `/nym-node/${id}`,
},
{
label: "Account",
isSelected: false,
link: `/account/${observatoryNymNode.bonding_address}`,
},
]}
/>
)}
</Box>
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<NodeProfileCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<BasicInfoCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<NodeRoleCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 6,
}}
>
<NodeParametersCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 6,
}}
>
<NodeDataCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
}}
>
<NodeDelegationsCard id={id} />
</Grid>
{/*
<Grid
size={{
xs: 12,
}}
>
<NodeChatCard />
</Grid> */}
</Grid>
</ContentLayout>
);
} catch (error) {
let errorMessage = "An error occurred";
if (error instanceof Error) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
}
@@ -1,155 +0,0 @@
import TableOfContents from "@/components/blogs/TableOfContents";
import type BlogArticle from "@/components/blogs/types";
import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import { Link } from "@/components/muiLink";
import { Wrapper } from "@/components/wrapper";
import { Box, Stack, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { format } from "date-fns";
import Image from "next/image";
import Markdown from "react-markdown";
export default async function BlogPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
try {
const blogArticle: BlogArticle = await import(`@/data/${slug}.json`);
const breadcrumbItems = [
{
label: "Onboarding",
href: "/onboarding",
},
{ label: blogArticle.title, isCurrentPage: true },
];
return (
<ContentLayout>
<Wrapper>
<Grid container spacing={5}>
<Grid size={{ xs: 12, md: 8 }}>
<Stack spacing={4}>
<Breadcrumbs items={breadcrumbItems} />
<SectionHeading title={blogArticle.title} />
<Box
sx={{
borderTop: "1px dashed",
paddingBlockStart: "10px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle3"
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
Author
{(blogArticle?.attributes?.blogAuthors?.length ?? 0) > 1
? "s"
: ""}
:{" "}
{blogArticle?.attributes?.blogAuthors?.map(
(author: string) => (
<Typography key={author} variant="subtitle3">
{author}
</Typography>
)
)}
</Box>
<time dateTime={blogArticle?.attributes?.date.toString()}>
{format(
new Date(blogArticle?.attributes?.date),
"MMMM dd, yyyy"
)}
</time>
</Typography>
<Typography variant="subtitle3">
{blogArticle.attributes.readingTime}{" "}
{blogArticle.attributes.readingTime > 1 ? "mins" : "min"}{" "}
read
</Typography>
</Box>
<Image
src={blogArticle.image}
alt="blog-image"
width={120}
height={60}
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
/>
<Box>
{blogArticle.overview.content.map(({ text }) => (
<Box key={text} sx={{ mt: 3 }}>
<Typography variant="body2" component="span">
<Markdown className="reactMarkDownLink reactMarkDownList">
{text}
</Markdown>
</Typography>
</Box>
))}
</Box>
{blogArticle.sections.map((section) => (
<Box key={section.heading} id={section.id}>
<SectionHeading title={section.heading} />
{section.text.map(({ text }) => (
<Box key={text} sx={{ mt: 3 }}>
<Typography variant="body2" component="span">
<Markdown className="reactMarkDownLink reactMarkDownList">
{text}
</Markdown>
</Typography>
</Box>
))}
</Box>
))}
</Stack>
</Grid>
<Grid size={{ md: 4 }}>
<TableOfContents
headings={blogArticle.sections.map((section) => ({
heading: section.heading,
id: section.id,
}))}
/>
</Grid>
</Grid>
</Wrapper>
</ContentLayout>
);
} catch (error) {
console.log(error);
return (
<ContentLayout>
<Wrapper>
<SectionHeading title={"Off the grid, like your data"} />
<Typography variant="body2">
Oops! Looks like the page youre looking for got mixed up in the
noise. Dont worry, your privacy is intact. Lets get you
<Link href="/">back to the homepage.</Link>
</Typography>
</Wrapper>
</ContentLayout>
);
}
}
@@ -1,15 +0,0 @@
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import Grid from "@mui/material/Grid2";
export default function OnboardingPage() {
return (
<ContentLayout>
<SectionHeading title="Onboarding page" />
<Grid container spacing={4}>
<BlogArticlesCards ids={[1, 2]} />
</Grid>
</ContentLayout>
);
}
@@ -1,24 +0,0 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
// import Grid from "@mui/material/Grid2";
import { ContentLayout } from "../../../components/contentLayout/ContentLayout";
import SectionHeading from "../../../components/headings/SectionHeading";
import OverviewCards from "../../../components/staking/OverviewCards";
import StakeTableWithAction from "../../../components/staking/StakeTableWithAction";
import SubHeaderRow from "../../../components/staking/SubHeaderRow";
export default async function StakingPage() {
return (
<ContentLayout>
<SectionHeading title="Staking" />
<SubHeaderRow />
<OverviewCards />
<StakeTableWithAction />
{/* <Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1]} />
</Grid> */}
</ContentLayout>
);
}
@@ -1,30 +0,0 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction";
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
import { Wrapper } from "@/components/wrapper";
import { Box, Stack } from "@mui/material";
// import Grid from "@mui/material/Grid2";
export default function ExplorerPage() {
return (
<ContentLayout>
<Wrapper>
<Stack gap={5}>
<SectionHeading title="Explorer" />
<NodeAndAddressSearch />
</Stack>
<Box sx={{ mt: 5 }}>
<NodeTableWithAction />
</Box>
{/* <Grid container columnSpacing={5} rowSpacing={5} mt={10}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards limit={2} />
</Grid> */}
</Wrapper>
</ContentLayout>
);
}
-271
View File
@@ -1,271 +0,0 @@
import { addSeconds } from "date-fns";
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
CurrentEpochData,
ExplorerData,
GatewayStatus,
IAccountBalancesInfo,
IObservatoryNode,
IPacketsAndStakingData,
NodeData,
NodeRewardDetails,
NymTokenomics,
ObservatoryBalance,
} from "./types";
import {
CURRENT_EPOCH,
CURRENT_EPOCH_REWARDS,
DATA_OBSERVATORY_BALANCES_URL,
DATA_OBSERVATORY_NODES_DELEGATIONS_URL,
DATA_OBSERVATORY_NODES_URL,
NS_API_MIXNODES_STATS,
NYM_ACCOUNT_ADDRESS,
NYM_NODES,
NYM_PRICES_API,
OBSERVATORY_GATEWAYS_URL,
} from "./urls";
// Fetch function for epoch rewards
export const fetchEpochRewards = async (): Promise<
ExplorerData["currentEpochRewardsData"]
> => {
const response = await fetch(CURRENT_EPOCH_REWARDS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
cache: "no-store", // Ensures fresh data on every request
});
if (!response.ok) {
throw new Error("Failed to fetch epoch rewards");
}
return response.json();
};
// Fetch gateway status based on identity key
export const fetchGatewayStatus = async (
identityKey: string,
): Promise<GatewayStatus | null> => {
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
if (!response.ok) {
throw new Error("Failed to fetch gateway status");
}
return response.json();
};
export const fetchNodeInfo = async (
id: number,
): Promise<IObservatoryNode | undefined> => {
const nodes = await fetchObservatoryNodes();
return nodes?.find((node) => node.node_id === id);
};
export const fetchNodeIdByIdentityKey = async (
identity_key: string,
): Promise<number> => {
const nodes = await fetchObservatoryNodes();
const node = nodes?.find((node) => node.identity_key === identity_key);
return node?.node_id || 0;
};
export const fetchNodeDelegations = async (
id: number,
): Promise<NodeRewardDetails[]> => {
const response = await fetch(
`${DATA_OBSERVATORY_NODES_DELEGATIONS_URL}/${id}/delegations`,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
},
);
if (!response.ok) {
throw new Error("Failed to fetch delegations");
}
return response.json();
};
export const fetchCurrentEpoch = async () => {
const response = await fetch(CURRENT_EPOCH, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
cache: "no-store", // Ensures fresh data on every request
});
if (!response.ok) {
throw new Error("Failed to fetch current epoch data");
}
const data: CurrentEpochData = await response.json();
const epochEndTime = addSeconds(
new Date(data.current_epoch_start),
data.epoch_length.secs,
).toISOString();
return { ...data, current_epoch_end: epochEndTime };
};
// Fetch balances based on the address
export const fetchBalances = async (address: string): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Calculate total stake
return (
Number(balances.rewards.staking_rewards.amount) +
Number(balances.delegated.amount)
);
};
// Fetch function to get total staker rewards
export const fetchTotalStakerRewards = async (
address: string,
): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Return the staking rewards amount
return Number(balances.rewards.staking_rewards.amount);
};
// Fetch function to get the original stake
export const fetchOriginalStake = async (address: string): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Return the delegated amount
return Number(balances.delegated.amount);
};
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
const response = await fetch(NS_API_MIXNODES_STATS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
const data: IPacketsAndStakingData[] = await response.json();
return data;
};
// Fetch Account Balance
export const fetchAccountBalance = async (
address: string,
): Promise<IAccountBalancesInfo> => {
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch account balance error from api");
}
const data: IAccountBalancesInfo = await res.json();
return data;
};
// 🔹 Fetch Nodes
export const fetchNodes = async (): Promise<NodeData[]> => {
const res = await fetch(NYM_NODES, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch nodes");
}
const data: NodeData[] = await res.json();
return data;
};
export const fetchObservatoryNodes = async (): Promise<IObservatoryNode[]> => {
const allNodes: IObservatoryNode[] = [];
let page = 1;
const PAGE_SIZE = 200;
let hasMoreData = true;
while (hasMoreData) {
const response = await fetch(
`${DATA_OBSERVATORY_NODES_URL}?page=${page}&limit=${PAGE_SIZE}`,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
},
);
if (!response.ok) {
throw new Error(`Failed to fetch observatory nodes (page ${page})`);
}
const nodes: IObservatoryNode[] = await response.json();
allNodes.push(...nodes);
if (nodes.length < PAGE_SIZE) {
hasMoreData = false; // Stop fetching when the last page has fewer than 200 items
} else {
page++; // Move to the next page
}
}
return allNodes;
};
// 🔹 Fetch NYM Price
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
const res = await fetch(NYM_PRICES_API, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch NYM price");
}
const data: NymTokenomics = await res.json();
return data;
};
-23417
View File
File diff suppressed because it is too large Load Diff
-482
View File
@@ -1,482 +0,0 @@
export type API_RESPONSE<T> = {
data: T[];
};
export type Denom = "unym" | "nym";
export interface IPacketsAndStakingData {
date_utc: string;
total_packets_received: number;
total_packets_sent: number;
total_packets_dropped: number;
total_stake: number;
}
export interface CurrentEpochData {
id: number;
current_epoch_id: number;
current_epoch_start: string;
epoch_length: { secs: number; nanos: number };
epochs_in_interval: number;
total_elapsed_epochs: number;
}
export interface ExplorerData {
circulatingNymSupplyData: {
circulating_supply: { denom: Denom; amount: string };
mixmining_reserve: { denom: Denom; amount: string };
total_supply: { denom: Denom; amount: string };
vesting_tokens: { denom: Denom; amount: string };
};
nymNodesData: {
gateways: {
bonded: { count: number; last_updated_utc: string };
blacklisted: { count: number; last_updated_utc: string };
historical: { count: number; last_updated_utc: string };
explorer: { count: number; last_updated_utc: string };
};
mixnodes: {
bonded: {
count: number;
active: number;
inactive: number;
reserve: number;
last_updated_utc: string;
};
blacklisted: {
count: number;
last_updated_utc: string;
};
historical: { count: number; last_updated_utc: string };
};
};
packetsAndStakingData: IPacketsAndStakingData[];
currentEpochRewardsData: {
interval: {
reward_pool: string;
staking_supply: string;
staking_supply_scale_factor: string;
epoch_reward_budget: string;
stake_saturation_point: string;
active_set_work_factor: string;
interval_pool_emission: string;
sybil_resistance: string;
};
rewarded_set: {
entry_gateways: number;
exit_gateways: number;
mixnodes: number;
standby: number;
};
};
}
export type NodeDescription = {
last_polled: string;
host_information: {
ip_address: string[];
hostname: string;
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
declared_role: {
mixnode: boolean;
entry: boolean;
exit_nr: boolean;
exit_ipr: boolean;
};
auxiliary_details: {
location: string;
announce_ports: {
verloc_port: number | null;
mix_port: number | null;
};
accepted_operator_terms_and_conditions: boolean;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
commit_sha: string;
commit_timestamp: string;
commit_branch: string;
rustc_version: string;
rustc_channel: string;
cargo_profile: string;
cargo_triple: string;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
ip_packet_router: {
address: string;
};
authenticator: {
address: string;
};
wireguard: string | null;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
} | null;
export type BondInformation = {
node_id: number;
owner: string;
original_pledge: {
denom: string;
amount: string;
};
bonding_height: number;
is_unbonding: boolean;
node: {
host: string;
custom_http_port: number;
identity_key: string;
};
};
export type RewardingDetails = {
cost_params: {
profit_margin_percent: string;
interval_operating_cost: {
denom: string;
amount: string;
};
};
operator: string;
delegates: string;
total_unit_reward: string;
unit_delegation: string;
last_rewarded_epoch: number;
unique_delegations: number;
};
export type Location = {
two_letter_iso_country_code?: string;
three_letter_iso_country_code?: string;
country_name?: string;
latitude?: number;
longitude?: number;
};
export type NodeData = {
node_id: number;
contract_node_type: string;
description: NodeDescription;
bond_information: BondInformation;
rewarding_details: RewardingDetails;
location: Location;
};
// ACCOUNT BALANCES
export interface IRewardDetails {
amount_staked: IAmountDetails;
node_id: number;
node_still_fully_bonded: boolean;
rewards: IAmountDetails;
}
export interface IAmountDetails {
denom: string;
amount: string;
}
export interface IDelegationDetails {
node_id: number;
delegated: IAmountDetails;
height: number;
proxy: null | string;
}
export interface IAccountBalancesInfo {
accumulated_rewards: IRewardDetails[];
address: string;
balances: IAmountDetails[];
claimable_rewards: IAmountDetails;
delegations: IDelegationDetails[];
operator_rewards?: null | IAmountDetails;
total_delegations: IAmountDetails;
total_value: IAmountDetails;
vesting_account?: null | string;
}
export interface IObservatoryNode {
accepted_tnc: boolean;
bonded: boolean;
bonding_address: string;
description: {
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string | null;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string | null;
ip_address: string[];
};
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: string | null;
geoip: {
city: string;
country: string;
ip_address: string;
loc: string;
node_id: number;
org: string;
postal: string;
region: string;
};
};
identity_key: string;
ip_address: string;
node_id: number;
node_type: string;
original_pledge: number;
rewarding_details: {
cost_params: {
interval_operating_cost: {
amount: string;
denom: string;
};
profit_margin_percent: string;
};
delegates: string;
last_rewarded_epoch: number;
operator: string;
total_unit_reward: string;
unique_delegations: number;
unit_delegation: string;
};
self_description: {
details: string;
moniker: string;
security_contact: string;
website: string;
};
total_stake: number;
uptime: number;
}
export interface NodeRewardDetails {
amount: {
amount: string;
denom: string;
};
cumulative_reward_ratio: string;
height: number;
node_id: number;
owner: string;
}
export type LastProbeResult = {
gateway: string;
outcome: {
as_entry: {
can_connect: boolean;
can_route: boolean;
};
as_exit: {
can_connect: boolean;
can_route_ip_external_v4: boolean;
can_route_ip_external_v6: boolean;
can_route_ip_v4: boolean;
can_route_ip_v6: boolean;
};
wg: {
can_handshake_v4: boolean;
can_handshake_v6: boolean;
can_register: boolean;
can_resolve_dns_v4: boolean;
can_resolve_dns_v6: boolean;
ping_hosts_performance_v4: number;
ping_hosts_performance_v6: number;
ping_ips_performance_v4: number;
ping_ips_performance_v6: number;
};
};
};
export type GatewayStatus = {
blacklisted: boolean;
bonded: boolean;
config_score: number;
description: {
details: string;
moniker: string;
security_contact: string;
website: string;
};
explorer_pretty_bond: {
identity_key: string;
location: {
latitude: number;
longitude: number;
two_letter_iso_country_code: string;
};
owner: string;
pledge_amount: {
amount: string;
denom: string;
};
};
gateway_identity_key: string;
last_probe_log: string;
last_probe_result: LastProbeResult; // Reference to the separate type
last_testrun_utc: string;
last_updated_utc: string;
performance: number;
routing_score: number;
self_described: {
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string;
ip_address: string[];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: {
port: number;
public_key: string;
};
};
};
type BalanceDetails = {
amount: number;
denom: string;
};
export type ObservatoryRewards = {
operator_commissions: BalanceDetails;
staking_rewards: BalanceDetails;
unlocked: BalanceDetails;
};
export type ObservatoryBalance = {
delegated: BalanceDetails;
locked: BalanceDetails;
rewards: ObservatoryRewards;
self_bonded: BalanceDetails;
spendable: BalanceDetails;
};
export type Quote = {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_change_6h: number;
percent_change_7d: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
export type Quotes = {
USD: Quote;
};
export type NymTokenomics = {
beta_value: number;
first_data_at: string;
id: string;
last_updated: string;
max_supply: number;
name: string;
quotes: Quotes;
rank: number;
symbol: string;
total_supply: number;
};
-28
View File
@@ -1,28 +0,0 @@
export const NYM_NODES =
"https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes";
export const HARBOURMASTER_API_MIXNODES_STATS =
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
export const NS_API_MIXNODES_STATS =
"https://staging-node-status-api.nymte.ch/v2/mixnodes/stats";
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 NYM_NODE_BONDED =
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
export const NYM_ACCOUNT_ADDRESS =
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
export const VALIDATOR_BASE_URL =
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
export const DATA_OBSERVATORY_NODES_URL =
"https://api.nym.spectredao.net/api/v1/nodes";
export const DATA_OBSERVATORY_NODES_DELEGATIONS_URL =
"https://api.nym.spectredao.net/api/v1/nodes";
export const DATA_OBSERVATORY_DELEGATIONS_URL =
"https://api.nym.spectredao.net/api/v1/delegations";
export const DATA_OBSERVATORY_BALANCES_URL =
"https://api.nym.spectredao.net/api/v1/balances";
export const OBSERVATORY_GATEWAYS_URL =
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
-1
View File
@@ -1 +0,0 @@
export const TABLET_WIDTH = "(min-width:700px)";
-27
View File
@@ -1,27 +0,0 @@
"use client";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import { Link } from "@/components/muiLink";
import { Button, Stack, Typography } from "@mui/material";
const ErrorPage = ({ error }: { error: Error }) => {
return (
<ContentLayout>
<Stack spacing={2} justifyContent="flex-start">
<Typography variant="body1">
An error occurred: {error.message}
</Typography>
<Typography variant="body2">
Please try again later or contact support
</Typography>
<Link href="/" underline="none">
<Button variant="contained" sx={{ maxWidth: 100 }} size="small">
Home
</Button>
</Link>
</Stack>
</ContentLayout>
);
};
export default ErrorPage;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,34 +0,0 @@
// API
import { client } from "../../../lib/strapiClient";
// Types
import type { Languages } from "../../../i18n";
import type { components } from "@/app/lib/strapi";
// Constants
import { footerApiPath } from "../../footer/config/constants";
// Fetch footer data
export const getFooter = async (
locale: Languages,
): Promise<{
id?: number;
attributes?: components["schemas"]["Footer"];
} | null> => {
const footer = await client.GET(footerApiPath, {
params: {
query: {
locale,
// @ts-expect-error - populate is not typed correctly?
populate: {
linkBlocks: {
populate: "*",
},
},
},
},
});
return footer?.data?.data ? footer?.data?.data : null;
};
@@ -1,72 +0,0 @@
// Types
import type { components } from "../../../lib/strapi";
// Components
import { Link } from "@/components/muiLink";
// MUI Components
import { Box, Grid2, Typography } from "@mui/material";
export const FooterLinks = ({
linkBlocks = [],
}: {
linkBlocks: components["schemas"]["FooterLinkBlockComponent"][];
}) => {
return (
<Grid2
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 1, sm: 8, md: 5 }}
>
{linkBlocks?.map((block) => {
return (
<Grid2 key={block.id} size={{ xs: 1, sm: 4, md: 1 }}>
<Typography
component={block?.heading?.level || "h3"}
variant="subtitle1"
sx={{ mb: 4 }}
>
{block?.heading?.title}
</Typography>
<Box
component={"ul"}
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{block?.links?.map((link) => {
const isLinkExternal = link.url?.startsWith("http");
return (
<Box
sx={{
listStyle: "none",
}}
component={"li"}
key={link.id}
>
<Link
href={link?.url || ""}
sx={{
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
<Typography variant="body3">
{link.title}
{isLinkExternal ? " ↗" : ""}
</Typography>
</Link>
</Box>
);
})}
</Box>
</Grid2>
);
})}
</Grid2>
);
};
@@ -1 +0,0 @@
export const footerApiPath = "/footer";
-68
View File
@@ -1,68 +0,0 @@
:root {
--max-width: 1120px;
--border-radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
.MuiCardActionArea-focusHighlight {
background: transparent;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar {
right: 0;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background-color: #aaa;
border-radius: 8px;
width: 8px;
height: 8px;
}
.reactMarkDownLink a {
color: #000000;
display: inline;
font-weight: 700;
}
.reactMarkDownList ul {
margin-left: 20px;
}
.reactMarkDownList ol {
margin-left: 20px;
}
-41
View File
@@ -1,41 +0,0 @@
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { getOptions } from "./settings";
import type { Languages } from "./types";
const initI18next = async ({ lng, ns }: { lng: Languages; ns?: string }) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`),
),
)
.init(getOptions(lng, ns));
return i18nInstance;
};
export const useTranslation = async ({
lng,
ns,
options,
}: {
lng: Languages;
ns?: string;
options?: { keyPrefix: string };
}) => {
const i18nextInstance = await initI18next({ lng, ns });
return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options?.keyPrefix,
),
i18n: i18nextInstance,
};
};
export type { Languages, GeneralTranslations } from "./types";
@@ -1,92 +0,0 @@
{
"langSelectorLabel": "Language",
"socialLinks": [
{ "name": "Telegram", "link": "https://nymtech.net/go/telegram" },
{ "name": "Twitter", "link": "https://nymtech.net/go/x" },
{
"name": "Discord",
"link": "https://nymtech.net/go/discord"
},
{ "name": "GitHub", "link": "https://nymtech.net/go/github" },
{ "name": "YouTube", "link": "https://nymtech.net/go/youtube" }
],
"links": [
{
"title": "Get started",
"links": [
{
"name": "Download NymVPN",
"link": "download"
},
{
"name": "Test NymVPN",
"link": "alpha"
},
{
"name": "Become an operator",
"link": "https://nymtech.net/operators"
},
{ "name": "Visit nymtech.net", "link": "https://nymtech.net" },
{
"name": "Subscribe to Nyms newsletter",
"link": "https://eepurl.com/gdor_f"
}
]
},
{
"title": "Popular blog posts",
"links": [
{ "name": "Decentralized VPNs", "link": "blog/decentralized-vpns" },
{
"name": "Blockchain-based VPNs",
"link": "blog/blockchain-based-vpns-all-you-need-to-know"
},
{
"name": "Privacy protection with VPN",
"link": "blog/how-does-a-vpn-protect-you-and-your-privacy"
},
{
"name": "Tracking prevention with VPN",
"link": "blog/can-you-be-tracked-while-using-a-vpn"
},
{
"name": "Online privacy threats",
"link": "blog/internet-privacy-main-threats-and-protections"
}
]
},
{
"title": "Resources",
"links": [
{ "name": "NymVPN blog", "link": "blog" },
{
"name": "NymVPN public roadmap",
"link": "https://trello.com/b/qVhBo3e2/nymvpn-public-roadmap"
},
{
"name": "NymVPN localization",
"link": "https://weblate.nymte.ch/projects/nymvpn/"
}
]
},
{
"title": "Company",
"links": [
{ "name": "Contact", "link": "contact" },
{
"name": "Careers",
"link": "https://nym.teamtailor.com/"
},
{
"name": "Support",
"link": "https://support.nymvpn.com/hc/en-us"
},
{ "name": "Imprint", "link": "imprint" },
{ "name": "Privacy statements", "link": "privacy" },
{ "name": "Terms of use", "link": "terms" }
]
}
],
"trademarkText": "WireGuard is a registered trademark of Jason A. Donenfeld",
"rightsText": "© 2024 Nym Technologies S.A., all rights reserved"
}
@@ -1,12 +0,0 @@
{
"socialChannels": [
{ "name": "Telegram", "link": "https://nym.com/go/telegram" },
{ "name": "Twitter", "link": "https://nym.com/go/x" },
{
"name": "Discord",
"link": "https://nym.com/go/discord"
},
{ "name": "GitHub", "link": "https://nym.com/go/github" },
{ "name": "YouTube", "link": "https://nym.com/go/youtube" }
]
}
-16
View File
@@ -1,16 +0,0 @@
export const fallbackLng = "en";
export const languages = [fallbackLng, "es"] as const;
export const defaultNS = "translation";
export const cookieName = "i18next";
export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}
@@ -1,46 +0,0 @@
export interface RedeemFreePassCopy {
errors: {
invalidCode: string;
alreadyRedeemed: string;
unknown: string;
};
}
type GetStarted = {
title: string;
description: string;
ctaText: string;
inputLabel: string;
prompt: string;
};
export type AlphaPageSteps = {
title: string;
description: string;
};
export type AlphaPageTranslations = {
hero: {
title: {
firstLine: string;
secondLine?: string;
};
getCredential: GetStarted;
signUp: GetStarted;
imageAlt: string;
stepsTitle: string;
finalSteps: AlphaPageSteps[];
finalStepsAlert: string;
credentialsTitle: string;
credentialsButton: string;
};
redeemFreePass: RedeemFreePassCopy;
firstSection: {
title: string;
content: string[];
};
secondSection: {
title: string;
content: string[];
};
};
-13
View File
@@ -1,13 +0,0 @@
import type { languages } from "../settings";
export type Languages = (typeof languages)[number];
// They don't belong to any specific page, that's why I defined them here, but open to suggestions.
export type GeneralTranslations = {
alert: string;
copyToClipboard: {
copy: string;
copied: string;
copiedTimeLimitation: string;
};
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

-30
View File
@@ -1,30 +0,0 @@
import { Header } from "@/components/header";
import { Wrapper } from "@/components/wrapper";
import Providers from "@/providers";
import type { Metadata } from "next";
import "./globals.css";
import "@interchain-ui/react/styles";
import { Footer } from "@/components/footer";
export const metadata: Metadata = {
title: "Nym Explorer V2",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<Providers>
<Header />
<Wrapper>{children}</Wrapper>
<Footer />
</Providers>
</body>
</html>
);
}
-23417
View File
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
import createClient from "openapi-fetch";
import qs from "qs";
import type { paths } from "./strapi";
if (!process.env.NEXT_PUBLIC_CMS_API_URL) {
throw new Error(
"NEXT_PUBLIC_CMS_API_URL environment variable is not defined",
);
}
if (!process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE) {
throw new Error(
"NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE environment variable is not defined",
);
}
const client = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_CMS_API_URL,
headers: {
Accept: "application/json",
},
fetch: (request: unknown) => {
const req = request as Request;
const url = new URL(req.url, process.env.NEXT_PUBLIC_CMS_API_URL);
return fetch(new Request(url, req), {
next: {
revalidate: Number(process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE),
},
});
},
querySerializer(params) {
return qs.stringify(params, {
encodeValuesOnly: true,
});
},
});
export { client };
-67
View File
@@ -1,67 +0,0 @@
import { Stack, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
import { ContentLayout } from "../components/contentLayout/ContentLayout";
import SectionHeading from "../components/headings/SectionHeading";
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
import { RewardsCard } from "../components/landingPageComponents/StakersNumberCard";
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
import NodeTable from "../components/nodeTable/NodeTableWithAction";
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
export default async function Home() {
return (
<ContentLayout>
<Stack gap={5}>
<Typography variant="h1" textTransform={"uppercase"}>
Mixnet in your hands
</Typography>
<NodeAndAddressSearch />
</Stack>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Noise Generating Mixnet Overview" />
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<NoiseCard />
</Grid>
<Grid
container
columnSpacing={5}
rowSpacing={5}
size={{ xs: 12, sm: 6, md: 3 }}
>
<Grid size={12}>
<RewardsCard />
</Grid>
<Grid size={12}>
<CurrentEpochCard />
</Grid>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<NetworkStakeCard />
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<TokenomicsCard />
</Grid>
</Grid>
<Grid container>
<Grid size={12}>
<SectionHeading title="Nym Nodes" />
</Grid>
<Grid size={12}>
<NodeTable />
</Grid>
</Grid>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1, 2]} />
</Grid>
</ContentLayout>
);
}
@@ -1,219 +0,0 @@
"use client";
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
import { Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import type { IRewardDetails } from "../../app/api/types";
import ExplorerCard from "../cards/ExplorerCard";
import { AccountBalancesTable } from "./AccountBalancesTable";
export interface IAccontStatsRowProps {
type: string;
allocation: number;
amount: number;
value: number;
history?: { type: string; amount: number }[];
isLastRow?: boolean;
progressBarColor?: string;
}
interface IAccountBalancesCardProps {
address: string;
}
const getNymsFormated = (unyms: number): number => {
if (unyms === 0) {
return 0;
}
const balance = unyms / 1000000;
return balance;
};
const getPriceInUSD = (unyms: number, usdPrice: number): number => {
if (unyms === 0) {
return 0;
}
const balanceInUSD = (unyms / 1000000) * usdPrice;
const balanceFormated = Number(balanceInUSD.toFixed(2));
return balanceFormated;
};
const getAllocation = (unyms: number, totalUnyms: number): number => {
if (unyms === 0) {
return 0;
}
const allocationPercentage = (unyms * 100) / totalUnyms;
return Number(allocationPercentage.toFixed(2));
};
const calculateStakingRewards = (
accumulatedRewards: IRewardDetails[],
): number => {
if (accumulatedRewards.length > 0) {
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
return total + Number.parseFloat(rewardDetail.rewards.amount);
}, 0);
const result = getNymsFormated(totalRewards);
return result;
}
return 0;
};
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
const { address } = props;
const {
data: accountInfo,
isLoading,
isError,
} = useQuery({
queryKey: ["accountBalance", address],
queryFn: () => fetchAccountBalance(address),
enabled: !!address,
});
const {
data: nymPrice,
isLoading: isLoadingPrice,
error: priceError,
} = useQuery({
queryKey: ["nymPrice"],
queryFn: fetchNymPrice,
});
if (isLoading || isLoadingPrice) {
return (
<ExplorerCard label="Total value">
<Stack gap={1}>
<Skeleton variant="text" height={38} />
<Skeleton variant="text" height={380} />
</Stack>
</ExplorerCard>
);
}
if (isError || priceError || !accountInfo || !nymPrice) {
return (
<ExplorerCard label="Total value">
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
Failed to account data.
</Typography>
<Skeleton variant="text" height={238} />
</ExplorerCard>
);
}
const nymPriceData = nymPrice.quotes.USD.price;
const totalBalanceUSD = getPriceInUSD(
Number(accountInfo.total_value.amount),
nymPriceData,
);
const spendableNYM =
accountInfo.balances.length > 0
? getNymsFormated(Number(accountInfo.balances[0].amount))
: 0;
const spendableUSD =
accountInfo.balances.length > 0
? getPriceInUSD(Number(accountInfo.balances[0].amount), nymPriceData)
: 0;
const spendableAllocation =
accountInfo.balances.length > 0
? getAllocation(
Number(accountInfo.balances[0].amount),
Number(accountInfo.total_value.amount),
)
: 0;
const delegationsNYM = getNymsFormated(
Number(accountInfo.total_delegations.amount),
);
const delegationsUSD = getPriceInUSD(
Number(accountInfo.total_delegations.amount),
nymPriceData,
);
const delegationsAllocation = getAllocation(
Number(accountInfo.total_delegations.amount),
Number(accountInfo.total_value.amount),
);
const operatorRewardsAllocation = getAllocation(
Number(accountInfo.operator_rewards?.amount || 0),
Number(accountInfo.total_value.amount),
);
const operatorRewardsNYM = getNymsFormated(
Number(accountInfo.operator_rewards?.amount || 0),
);
const operatorRewardsUSD = getPriceInUSD(
Number(accountInfo.operator_rewards?.amount || 0),
nymPriceData,
);
const claimableNYM = getNymsFormated(
Number(accountInfo.claimable_rewards.amount),
);
const claimableUSD = getPriceInUSD(
Number(accountInfo.claimable_rewards.amount),
nymPriceData,
);
const claimableAllocation = getAllocation(
Number(accountInfo.claimable_rewards.amount),
Number(accountInfo.total_value.amount),
);
const stakingRewards =
accountInfo.accumulated_rewards.length > 0
? calculateStakingRewards(accountInfo.accumulated_rewards)
: 0;
const tableRows = [
{
type: "Spendable",
allocation: spendableAllocation,
amount: spendableNYM,
value: spendableUSD,
},
{
type: "Delegated",
allocation: delegationsAllocation,
amount: delegationsNYM,
value: delegationsUSD,
// history: [
// { type: "Liquid", amount: 6900 },
// { type: "Locked", amount: 6900 },
// ],
},
{
type: "Claimable",
allocation: claimableAllocation,
amount: claimableNYM,
value: claimableUSD,
history: [
// { type: "Unlocked", amount: 6900 },
{
type: "Staking rewards",
amount: stakingRewards,
},
{ type: "Operator comission", amount: 0 },
],
},
{
type: "Operator Rewards",
allocation: operatorRewardsAllocation,
amount: operatorRewardsNYM,
value: operatorRewardsUSD,
},
];
return (
<ExplorerCard
label="Total value"
title={`$ ${totalBalanceUSD}`}
sx={{ height: "100%" }}
>
<AccountBalancesTable rows={tableRows} />
</ExplorerCard>
);
};
@@ -1,321 +0,0 @@
"use client";
import CircleIcon from "@mui/icons-material/Circle";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
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 useMediaQuery from "@mui/material/useMediaQuery";
import * as React from "react";
import { TABLET_WIDTH } from "../../app/constants";
import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar";
import { StaticProgressBar } from "../progressBars/StaticProgressBar";
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 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 variant="body4" sx={{ color: "pine.950" }}>
{type}
</Typography>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "25%",
}}
>
<Box>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{allocation}%
</Typography>
<StaticProgressBar
value={allocation}
color={progressBarColor || "green"}
/>
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{amount.toFixed(4)} NYM
</Typography>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
<Typography
variant="subtitle2"
sx={{ color: "pine.950", fontWeight: 700 }}
>
${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>
) : (
// 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: 12 }} />
{type}
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "45%",
}}
>
<Typography>{amount.toFixed(4)} 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) => (
<TableRow key={historyRow.type}>
<TableCell
sx={{
display: "flex",
alignItems: "center",
pl: 5,
borderBottom: "none", // Explicitly remove border
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
<span style={{ marginRight: 8 }}></span>
{historyRow.type}
</Typography>
</TableCell>
{tablet && (
<TableCell
sx={{
borderBottom: "none", // Explicitly remove border
}}
/>
)}
<TableCell
align="right"
sx={{
borderBottom: "none", // Explicitly remove border
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{historyRow.amount.toFixed(4)} NYM
</Typography>
</TableCell>
<TableCell
sx={{
borderBottom: "none", // Explicitly remove border
}}
/>
</TableRow>
))}
</React.Fragment>
);
};
export interface IAccountBalancesTableProps {
rows: Array<IAccontStatsRowProps>;
}
export const AccountBalancesTable = (props: IAccountBalancesTableProps) => {
const { rows } = props;
const tablet = useMediaQuery(TABLET_WIDTH);
const progressBarPercentages = () => {
return rows.map((row) => 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 (
<Box>
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
<TableContainer>
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
<TableHead>
{tablet ? (
<TableRow>
<TableCell>
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Type
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Allocation
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Amount
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Value
</Typography>
</TableCell>
<TableCell />
</TableRow>
) : (
<TableRow>
<TableCell>
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Type
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Amount / Value
</Typography>
</TableCell>
<TableCell />
</TableRow>
)}
</TableHead>
<TableBody>
{rows.map((row, i) => (
<Row
key={row.type}
{...row}
isLastRow={i === rows.length - 1}
progressBarColor={progressBarColours[i]}
/>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
@@ -1,84 +0,0 @@
"use client";
import { fetchAccountBalance } from "@/app/api";
import { Box, Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import ExplorerCard from "../cards/ExplorerCard";
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
import ExplorerListItem from "../list/ListItem";
import { CardQRCode } from "../qrCode/QrCode";
interface IAccountInfoCardProps {
address: string;
}
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
const { address } = props;
const { data, isLoading, isError } = useQuery({
queryKey: ["accountBalance", address],
queryFn: () => fetchAccountBalance(address),
enabled: !!address,
});
if (isLoading) {
return (
<ExplorerCard label="Total NYM">
<Stack gap={1}>
<Skeleton variant="text" height={38} />
<Skeleton variant="rectangular" height={128} width={128} />
<Skeleton variant="text" height={300} />
</Stack>
</ExplorerCard>
);
}
if (isError || !data) {
return (
<ExplorerCard label="Total NYM">
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
Failed to account data.
</Typography>
<Skeleton variant="text" height={238} />
</ExplorerCard>
);
}
const balance =
data.balances.length > 0 ? Number(data.total_value.amount) / 1000000 : 0;
const balanceFormated = `${balance.toFixed(4)} NYM`;
return (
<ExplorerCard
label="Total NYM"
title={balanceFormated}
sx={{ height: "100%" }}
>
<Stack gap={5}>
<Box display={"flex"} justifyContent={"flex-start"}>
<CardQRCode url={data.address} />
</Box>
<ExplorerListItem
label="Address"
value={
<Stack
direction="row"
gap={0.1}
alignItems="center"
justifyContent="space-between"
width="100%"
>
<Typography
variant="body4"
sx={{ wordWrap: "break-word", maxWidth: "85%" }}
>
{data.address}
</Typography>
<CopyToClipboard text={data.address} />
</Stack>
}
/>
</Stack>
</ExplorerCard>
);
};
@@ -1,86 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import Grid from "@mui/material/Grid2";
import ExplorerHeroCard from "../cards/ExplorerHeroCard";
import type { BlogArticleWithLink } from "./types";
import { icons, IconName } from "@/utils/getIconByName";
// TODO: Articles should be sorted by date
const BlogArticlesCards = async ({
limit,
ids,
}: {
limit?: number;
ids?: Array<number>;
}) => {
const blogsDir = path.join(process.cwd(), "/src/data");
const blogsDirFilenames = await fs.readdir(blogsDir);
// Read all blog articles from the data directory
const blogArticles: BlogArticleWithLink[] = await Promise.all(
blogsDirFilenames.map(async (filename) => {
const filePath = path.join(blogsDir, filename);
const fileContent = await fs.readFile(filePath, "utf-8");
const blogArticle = JSON.parse(fileContent);
return {
...blogArticle,
link: `/onboarding/${filename.replace(".json", "")}`,
};
}),
);
const limitedOrFilteredBlogArticles = (
blogArticles: BlogArticleWithLink[],
limit?: number,
ids?: number[],
): BlogArticleWithLink[] => {
let filteredArticles = blogArticles;
// Filter by IDs if provided
if (ids && ids.length > 0) {
filteredArticles = filteredArticles.filter((article) =>
ids.includes(article.id),
);
}
// Apply limit if provided
if (limit) {
filteredArticles = filteredArticles.slice(0, limit);
}
return filteredArticles;
};
const articles = limitedOrFilteredBlogArticles(blogArticles, limit, ids);
return articles
.sort((a, b) => {
// sort by date
return (
new Date(b.attributes.date).getTime() -
new Date(a.attributes.date).getTime()
);
})
.map((blogArticle) => {
return (
<Grid
size={{
sm: 12,
md: 6,
}}
key={blogArticle.title}
>
<ExplorerHeroCard
label={blogArticle.label}
title={blogArticle.title}
description={blogArticle.description}
icon={icons[blogArticle.icon as IconName]?.src}
link={blogArticle.link || ""}
sx={{ height: "100%" }}
/>
</Grid>
);
});
};
export default BlogArticlesCards;

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