Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eddb12cbe | |||
| e3fdc9aa12 | |||
| 4e7ece13c5 | |||
| 38f2a052d2 | |||
| 565e9c5a40 | |||
| b6465836d8 | |||
| 40916f77da | |||
| 492fe16e55 | |||
| cf3baf9398 | |||
| c3f462c34d | |||
| b95a026ddd | |||
| 48a38b6bf3 | |||
| 57e6fa29db | |||
| 1856ac95c6 | |||
| bc6d4562d0 | |||
| 4887cbbd48 | |||
| 3c44ae89da | |||
| a9c9381cb6 | |||
| fbd8cc5b4d | |||
| 399e4b1abd | |||
| fd62ee8204 | |||
| 147ec12a28 | |||
| c740f84336 | |||
| 16de47ba57 | |||
| 54a823311b | |||
| 753a21f8ca | |||
| 76da4ab532 | |||
| 2ca7c7a252 | |||
| e680e8dc49 | |||
| 242bc93807 | |||
| 94c6cdc7b2 | |||
| fce322c789 | |||
| ac5baab693 | |||
| 23da0f4d8e | |||
| 25e3b4cd83 | |||
| 8e4d72a565 | |||
| ad84a6d85d | |||
| 34c5f23684 | |||
| 000f2f1c29 | |||
| 317f7fffa9 | |||
| 4396def133 | |||
| a56a318a7f | |||
| 4d08047c57 | |||
| cb13be27f8 | |||
| fa392169c1 | |||
| 3167fb34e6 | |||
| 9ca6301e1c | |||
| e16a73338e | |||
| bfa3825d70 | |||
| d626e7689f | |||
| 9234474565 | |||
| 29f8386b50 | |||
| 0edb9631a6 | |||
| 4b0153f5f2 | |||
| c09a17b66d | |||
| d18ddcdc11 | |||
| d2df542280 | |||
| 6fafd8c03a | |||
| 38e66f6ddf | |||
| b9fbe0b8f3 | |||
| daafb5cae4 |
@@ -1,45 +0,0 @@
|
||||
name: ci-nym-credential-proxy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'common/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- '.github/workspace/ci-nym-credential-proxy.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MANIFEST_PATH: "--manifest-path nym-credential-proxy/Cargo.toml"
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: ${{ env.MANIFEST_PATH }} --all -- --check
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
|
||||
@@ -19,33 +19,33 @@ members = [
|
||||
"clients/native",
|
||||
"clients/native/websocket-requests",
|
||||
"clients/socks5",
|
||||
"common/authenticator-requests",
|
||||
"common/async-file-watcher",
|
||||
"common/authenticator-requests",
|
||||
"common/bandwidth-controller",
|
||||
"common/bin-common",
|
||||
"common/client-core",
|
||||
"common/client-core/config-types",
|
||||
"common/client-core/surb-storage",
|
||||
"common/client-core/gateways-storage",
|
||||
"common/client-core/surb-storage",
|
||||
"common/client-libs/gateway-client",
|
||||
"common/client-libs/mixnet-client",
|
||||
"common/client-libs/validator-client",
|
||||
"common/commands",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/country-group",
|
||||
"common/credential-storage",
|
||||
"common/credentials",
|
||||
"common/credential-utils",
|
||||
"common/credentials-interface",
|
||||
"common/credential-verification",
|
||||
"common/credentials",
|
||||
"common/credentials-interface",
|
||||
"common/crypto",
|
||||
"common/dkg",
|
||||
"common/ecash-double-spending",
|
||||
@@ -65,10 +65,10 @@ members = [
|
||||
"common/network-defaults",
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nymcoconut",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nym-id",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymcoconut",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
@@ -104,20 +104,23 @@ members = [
|
||||
"gateway",
|
||||
"integrations/bity",
|
||||
"mixnode",
|
||||
"sdk/ffi/cpp",
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/lib/socks5-listener",
|
||||
"sdk/rust/nym-sdk",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/cpp",
|
||||
"service-providers/authenticator",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"nym-network-monitor",
|
||||
"nym-api",
|
||||
"nym-browser-extension/storage",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-browser-extension/storage",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-credential-proxy/vpn-api-lib-wasm",
|
||||
"nym-data-observatory",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
"nym-node/nym-node-http-api",
|
||||
"nym-node/nym-node-requests",
|
||||
@@ -140,11 +143,11 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -155,6 +158,7 @@ default-members = [
|
||||
"gateway",
|
||||
"mixnode",
|
||||
"nym-api",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-data-observatory",
|
||||
"nym-node",
|
||||
"nym-node-status-api",
|
||||
@@ -193,16 +197,14 @@ aead = "0.5.2"
|
||||
anyhow = "1.0.90"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.83"
|
||||
axum-client-ip = "0.6.1"
|
||||
axum = "0.7.5"
|
||||
axum-extra = "0.9.4"
|
||||
base64 = "0.22.1"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
|
||||
# can we unify those?
|
||||
bit-vec = "0.7.0"
|
||||
bit-vec = "0.7.0" # can we unify those?
|
||||
bitvec = "1.0.0"
|
||||
|
||||
blake3 = "1.5.4"
|
||||
bloomfilter = "1.0.14"
|
||||
bs58 = "0.5.1"
|
||||
@@ -269,7 +271,7 @@ ipnetwork = "0.20"
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.13.0"
|
||||
k256 = "0.13"
|
||||
lazy_static = "1.4.0"
|
||||
lazy_static = "1.5.0"
|
||||
ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
log = "0.4"
|
||||
@@ -279,7 +281,7 @@ moka = { version = "0.12", features = ["future"] }
|
||||
nix = "0.27.1"
|
||||
notify = "5.1.0"
|
||||
okapi = "0.7.0"
|
||||
once_cell = "1.7.2"
|
||||
once_cell = "1.20.2"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
parking_lot = "0.12.3"
|
||||
@@ -407,7 +409,6 @@ wasm-bindgen-futures = "0.4.45"
|
||||
wasmtimer = "0.2.0"
|
||||
web-sys = "0.3.72"
|
||||
|
||||
|
||||
# Profile settings for individual crates
|
||||
|
||||
# Compile-time verified queries do quite a bit of work at compile time. Incremental
|
||||
|
||||
@@ -112,7 +112,7 @@ impl GeoAwareTopologyProvider {
|
||||
async fn get_topology(&self) -> Option<NymTopology> {
|
||||
let mixnodes = match self
|
||||
.validator_client
|
||||
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
|
||||
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::NymTopologyError;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -18,7 +17,11 @@ use wasmtimer::tokio::sleep;
|
||||
|
||||
mod accessor;
|
||||
pub mod geo_aware_provider;
|
||||
pub(crate) mod nym_api_provider;
|
||||
pub mod nym_api_provider;
|
||||
|
||||
pub use geo_aware_provider::GeoAwareTopologyProvider;
|
||||
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
|
||||
pub use nym_topology::provider_trait::TopologyProvider;
|
||||
|
||||
// TODO: move it to config later
|
||||
const MAX_FAILURE_COUNT: usize = 10;
|
||||
|
||||
@@ -14,9 +14,10 @@ use url::Url;
|
||||
pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
|
||||
pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
|
||||
|
||||
pub(crate) struct Config {
|
||||
pub(crate) min_mixnode_performance: u8,
|
||||
pub(crate) min_gateway_performance: u8,
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub min_mixnode_performance: u8,
|
||||
pub min_gateway_performance: u8,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -29,7 +30,7 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NymApiTopologyProvider {
|
||||
pub struct NymApiTopologyProvider {
|
||||
config: Config,
|
||||
|
||||
validator_client: nym_validator_client::client::NymApiClient,
|
||||
@@ -40,7 +41,7 @@ pub(crate) struct NymApiTopologyProvider {
|
||||
}
|
||||
|
||||
impl NymApiTopologyProvider {
|
||||
pub(crate) fn new(
|
||||
pub fn new(
|
||||
config: Config,
|
||||
mut nym_api_urls: Vec<Url>,
|
||||
client_version: String,
|
||||
@@ -98,7 +99,7 @@ impl NymApiTopologyProvider {
|
||||
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
|
||||
let mixnodes = match self
|
||||
.validator_client
|
||||
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
|
||||
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
|
||||
@@ -121,7 +121,9 @@ pub async fn current_mixnodes<R: Rng>(
|
||||
|
||||
log::trace!("Fetching list of mixnodes from: {nym_api}");
|
||||
|
||||
let mixnodes = client.get_basic_active_mixing_assigned_nodes(None).await?;
|
||||
let mixnodes = client
|
||||
.get_all_basic_active_mixing_assigned_nodes(None)
|
||||
.await?;
|
||||
let valid_mixnodes = mixnodes
|
||||
.iter()
|
||||
.filter_map(|mixnode| mixnode.try_into().ok())
|
||||
|
||||
@@ -19,7 +19,7 @@ use nym_api_requests::ecash::{
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::SkimmedNode;
|
||||
@@ -30,10 +30,10 @@ use time::Date;
|
||||
use url::Url;
|
||||
|
||||
pub use crate::nym_api::NymApiClientExt;
|
||||
use nym_mixnet_contract_common::NymNodeDetails;
|
||||
pub use nym_mixnet_contract_common::{
|
||||
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
|
||||
};
|
||||
|
||||
// re-export the type to not break existing imports
|
||||
pub use crate::coconut::EcashApiClient;
|
||||
|
||||
@@ -106,7 +106,9 @@ impl Config {
|
||||
|
||||
pub struct Client<C, S = NoSigner> {
|
||||
// ideally they would have been read-only, but unfortunately rust doesn't have such features
|
||||
// #[deprecated(note = "please use `nym_api_client` instead")]
|
||||
pub nym_api: nym_api::Client,
|
||||
// pub nym_api_client: NymApiClient,
|
||||
pub nyxd: NyxdClient<C, S>,
|
||||
}
|
||||
|
||||
@@ -243,6 +245,50 @@ impl<C, S> Client<C, S> {
|
||||
Ok(self.nym_api.get_gateways().await?)
|
||||
}
|
||||
|
||||
// TODO: combine with NymApiClient...
|
||||
pub async fn get_all_cached_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
|
||||
|
||||
descriptions.append(&mut res.data);
|
||||
if descriptions.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
// TODO: combine with NymApiClient...
|
||||
pub async fn get_all_cached_bonded_nym_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut bonds = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
|
||||
|
||||
bonds.append(&mut res.data);
|
||||
if bonds.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
request_body: &BlindSignRequestBody,
|
||||
@@ -290,7 +336,7 @@ impl NymApiClient {
|
||||
self.nym_api.change_base_url(new_endpoint);
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes instead")]
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
|
||||
pub async fn get_basic_mixnodes(
|
||||
&self,
|
||||
semver_compatibility: Option<String>,
|
||||
@@ -327,7 +373,7 @@ impl NymApiClient {
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_all_basic_entry_assigned_nodes(
|
||||
.get_basic_entry_assigned_nodes(
|
||||
semver_compatibility.clone(),
|
||||
false,
|
||||
Some(page),
|
||||
@@ -348,7 +394,7 @@ impl NymApiClient {
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
pub async fn get_basic_active_mixing_assigned_nodes(
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
@@ -378,6 +424,32 @@ impl NymApiClient {
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
/// retrieve basic information for all bonded nodes on the network
|
||||
pub async fn get_all_basic_nodes(
|
||||
&self,
|
||||
semver_compatibility: Option<String>,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_nodes(semver_compatibility.clone(), false, Some(page), None)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
pub async fn get_cached_active_mixnodes(
|
||||
&self,
|
||||
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
|
||||
@@ -404,6 +476,48 @@ impl NymApiClient {
|
||||
Ok(self.nym_api.get_gateways_described().await?)
|
||||
}
|
||||
|
||||
pub async fn get_all_described_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut descriptions = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
|
||||
|
||||
descriptions.append(&mut res.data);
|
||||
if descriptions.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(descriptions)
|
||||
}
|
||||
|
||||
pub async fn get_all_bonded_nym_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut bonds = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
|
||||
|
||||
bonds.append(&mut res.data);
|
||||
if bonds.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
pub async fn get_gateway_core_status_count(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
|
||||
@@ -11,9 +11,10 @@ use nym_api_requests::ecash::models::{
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse,
|
||||
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, NymNodeDescription,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
pub use nym_api_requests::{
|
||||
ecash::{
|
||||
models::{
|
||||
@@ -38,7 +39,7 @@ use nym_contracts_common::IdentityKey;
|
||||
pub use nym_http_api_client::Client;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
|
||||
use time::format_description::BorrowedFormatItem;
|
||||
use time::Date;
|
||||
use tracing::instrument;
|
||||
@@ -127,6 +128,44 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_nodes_described(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(&[routes::API_VERSION, "nym-nodes", "described"], ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_nym_nodes(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], ¶ms)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(
|
||||
&self,
|
||||
@@ -178,7 +217,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_all_basic_entry_assigned_nodes(
|
||||
async fn get_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
semver_compatibility: Option<String>,
|
||||
no_legacy: bool,
|
||||
@@ -259,6 +298,38 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_basic_nodes(
|
||||
&self,
|
||||
semver_compatibility: Option<String>,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(arg) = &semver_compatibility {
|
||||
params.push(("semver_compatibility", arg.clone()))
|
||||
}
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, "unstable", "nym-nodes", "skimmed"],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::{
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
/// Full details associated with given mixnode.
|
||||
@@ -647,14 +648,39 @@ impl From<LegacyMixLayer> for u8 {
|
||||
export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts"
|
||||
)
|
||||
)]
|
||||
#[cw_serde]
|
||||
#[derive(Default, Copy)]
|
||||
// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
|
||||
// with the addition of .cost_params_change field
|
||||
#[derive(
|
||||
::cosmwasm_schema::serde::Serialize,
|
||||
::cosmwasm_schema::serde::Deserialize,
|
||||
::std::clone::Clone,
|
||||
::std::fmt::Debug,
|
||||
::std::cmp::PartialEq,
|
||||
::cosmwasm_schema::schemars::JsonSchema,
|
||||
Default,
|
||||
Copy,
|
||||
)]
|
||||
#[schemars(crate = "::cosmwasm_schema::schemars")]
|
||||
pub struct PendingMixNodeChanges {
|
||||
pub pledge_change: Option<EpochEventId>,
|
||||
|
||||
#[serde(default)]
|
||||
pub cost_params_change: Option<IntervalEventId>,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LegacyPendingMixNodeChanges {
|
||||
pub pledge_change: Option<EpochEventId>,
|
||||
}
|
||||
|
||||
impl From<PendingMixNodeChanges> for LegacyPendingMixNodeChanges {
|
||||
fn from(value: PendingMixNodeChanges) -> Self {
|
||||
LegacyPendingMixNodeChanges {
|
||||
pledge_change: value.pledge_change,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PendingMixNodeChanges {
|
||||
pub fn new_empty() -> PendingMixNodeChanges {
|
||||
PendingMixNodeChanges {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
@@ -112,12 +112,18 @@ impl PemStorableKeyPair for KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub struct PublicKey(x25519_dalek::PublicKey);
|
||||
|
||||
impl Display for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_base58_string())
|
||||
Display::fmt(&self.to_base58_string(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&self.to_base58_string(), f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,16 @@ pub mod option_bs58_x25519_pubkey {
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<PublicKey>, D::Error> {
|
||||
let s = Option::<String>::deserialize(deserializer)?;
|
||||
s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
|
||||
.transpose()
|
||||
match Option::<String>::deserialize(deserializer)? {
|
||||
None => Ok(None),
|
||||
Some(s) => {
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
@@ -119,12 +119,18 @@ impl PemStorableKeyPair for KeyPair {
|
||||
}
|
||||
|
||||
/// ed25519 EdDSA Public Key
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub struct PublicKey(ed25519_dalek::VerifyingKey);
|
||||
|
||||
impl Display for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_base58_string())
|
||||
Display::fmt(&self.to_base58_string(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&self.to_base58_string(), f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ pub enum HttpClientError<E: Display = String> {
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("failed to deserialise received response: {source}")]
|
||||
ResponseDeserialisationFailure { source: serde_json::Error },
|
||||
|
||||
#[error("provided url is malformed: {source}")]
|
||||
MalformedUrl {
|
||||
#[from]
|
||||
@@ -531,19 +534,17 @@ where
|
||||
}
|
||||
|
||||
if res.status().is_success() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let text = res.text().await.inspect_err(|err| {
|
||||
tracing::error!("Couldn't even get response text: {err}");
|
||||
})?;
|
||||
tracing::trace!("Result:\n{:#?}", text);
|
||||
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|err| HttpClientError::GenericRequestFailure(err.to_string()))
|
||||
let text = res.text().await?;
|
||||
match serde_json::from_str(&text) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(source) => {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
tracing::trace!("Result:\n{:#?}", text);
|
||||
}
|
||||
Err(HttpClientError::ResponseDeserialisationFailure { source })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
Ok(res.json().await?)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound)
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ license.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum-client-ip.workspace = true
|
||||
axum.workspace = true
|
||||
bytes = { workspace = true }
|
||||
colored.workspace = true
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use axum::extract::{ConnectInfo, Request};
|
||||
use axum::extract::Request;
|
||||
use axum::http::header::{HOST, USER_AGENT};
|
||||
use axum::http::HeaderValue;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::IntoResponse;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use colored::Colorize;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
pub async fn logger(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
InsecureClientIp(addr): InsecureClientIp,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
|
||||
@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TestrunAssignment {
|
||||
/// has nothing to do with GW identity key. This is PK from `gateways` table
|
||||
pub testrun_id: i64,
|
||||
pub gateway_pk_id: i64,
|
||||
pub gateway_identity_key: String,
|
||||
}
|
||||
|
||||
@@ -286,6 +286,10 @@ impl NymTopology {
|
||||
self.get_gateway(gateway_identity).is_some()
|
||||
}
|
||||
|
||||
pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) {
|
||||
self.gateways.push(gateway)
|
||||
}
|
||||
|
||||
pub fn set_gateways(&mut self, gateways: Vec<gateway::LegacyNode>) {
|
||||
self.gateways = gateways
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode {
|
||||
});
|
||||
}
|
||||
|
||||
let layer = match value.epoch_role {
|
||||
let layer = match value.role {
|
||||
NodeRole::Mixnode { layer } => layer
|
||||
.try_into()
|
||||
.map_err(|_| MixnodeConversionError::InvalidLayer)?,
|
||||
|
||||
@@ -68,7 +68,7 @@ pub async fn current_network_topology_async(
|
||||
|
||||
let api_client = NymApiClient::new(url);
|
||||
let mixnodes = api_client
|
||||
.get_basic_active_mixing_assigned_nodes(None)
|
||||
.get_all_basic_active_mixing_assigned_nodes(None)
|
||||
.await?;
|
||||
let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?;
|
||||
|
||||
|
||||
@@ -158,10 +158,13 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
||||
.ok_or(Error::MissingClientBandwidthEntry)?
|
||||
.client_id
|
||||
{
|
||||
storage.create_bandwidth_entry(client_id).await?;
|
||||
let bandwidth = storage
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.ok_or(Error::MissingClientBandwidthEntry)?;
|
||||
Ok(Some(BandwidthStorageManager::new(
|
||||
storage,
|
||||
ClientBandwidth::new(Default::default()),
|
||||
ClientBandwidth::new(bandwidth.into()),
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
|
||||
@@ -84,12 +84,13 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
||||
.ok_or(Error::InconsistentConsumedBytes)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::InconsistentConsumedBytes)?;
|
||||
if bandwidth_manager
|
||||
.write()
|
||||
.await
|
||||
.try_use_bandwidth(spent_bandwidth)
|
||||
.await
|
||||
.is_err()
|
||||
if spent_bandwidth > 0
|
||||
&& bandwidth_manager
|
||||
.write()
|
||||
.await
|
||||
.try_use_bandwidth(spent_bandwidth)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let success = self.remove_peer().await?;
|
||||
return Ok(!success);
|
||||
|
||||
@@ -433,7 +433,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1483,9 +1483,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1660,7 +1660,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1702,9 +1702,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.214"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -1729,13 +1729,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.214"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1746,7 +1746,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1769,7 +1769,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1928,9 +1928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.59"
|
||||
version = "2.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a"
|
||||
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1954,7 +1954,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2120,5 +2120,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.59",
|
||||
"syn 2.0.85",
|
||||
]
|
||||
|
||||
@@ -3689,8 +3689,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
@@ -5239,8 +5238,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
@@ -5575,8 +5573,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
@@ -7595,8 +7592,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
|
||||
@@ -315,8 +315,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
|
||||
@@ -323,8 +323,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
|
||||
@@ -317,8 +317,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
|
||||
@@ -319,8 +319,7 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"Percent": {
|
||||
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use super::helpers::must_get_gateway_bond_by_owner;
|
||||
use super::storage;
|
||||
use crate::constants::default_node_costs;
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::nodes::helpers::save_new_nymnode_with_id;
|
||||
use crate::nodes::transactions::add_nym_node_inner;
|
||||
@@ -115,6 +116,10 @@ pub fn try_migrate_to_nymnode(
|
||||
comment: "legacy gateway did not have a pre-assigned node id".to_string(),
|
||||
})?;
|
||||
|
||||
let current_epoch =
|
||||
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
|
||||
let previous_epoch = current_epoch.saturating_sub(1);
|
||||
|
||||
// create nym-node entry
|
||||
// for gateways it's quite straightforward as there are no delegations or rewards to worry about
|
||||
save_new_nymnode_with_id(
|
||||
@@ -125,6 +130,7 @@ pub fn try_migrate_to_nymnode(
|
||||
cost_params,
|
||||
info.sender.clone(),
|
||||
gateway_bond.pledge_amount,
|
||||
previous_epoch,
|
||||
)?;
|
||||
|
||||
storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone());
|
||||
|
||||
@@ -22,6 +22,8 @@ pub(crate) fn save_new_nymnode(
|
||||
pledge: Coin,
|
||||
) -> Result<NodeId, MixnetContractError> {
|
||||
let node_id = next_nymnode_id_counter(storage)?;
|
||||
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
|
||||
|
||||
save_new_nymnode_with_id(
|
||||
storage,
|
||||
node_id,
|
||||
@@ -30,11 +32,13 @@ pub(crate) fn save_new_nymnode(
|
||||
cost_params,
|
||||
owner,
|
||||
pledge,
|
||||
current_epoch,
|
||||
)?;
|
||||
|
||||
Ok(node_id)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn save_new_nymnode_with_id(
|
||||
storage: &mut dyn Storage,
|
||||
node_id: NodeId,
|
||||
@@ -43,10 +47,9 @@ pub(crate) fn save_new_nymnode_with_id(
|
||||
cost_params: NodeCostParams,
|
||||
owner: Addr,
|
||||
pledge: Coin,
|
||||
last_rewarding_epoch: u32,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
|
||||
|
||||
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
|
||||
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, last_rewarding_epoch)?;
|
||||
let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height);
|
||||
|
||||
// save node bond data
|
||||
|
||||
@@ -52,8 +52,8 @@ pub(crate) fn save_assignment(
|
||||
|
||||
// update metadata
|
||||
let mut metadata = ROLES_METADATA.load(storage, inactive)?;
|
||||
let last = assignment.nodes.last().copied().unwrap_or_default();
|
||||
metadata.set_highest_id(last, assignment.role);
|
||||
let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default();
|
||||
metadata.set_highest_id(highest_id, assignment.role);
|
||||
metadata.set_role_count(assignment.role, assignment.nodes.len() as u32);
|
||||
if assignment.is_final_assignment() {
|
||||
metadata.fully_assigned = true
|
||||
@@ -140,6 +140,7 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
|
||||
#[test]
|
||||
fn next_id() {
|
||||
@@ -149,4 +150,33 @@ mod tests {
|
||||
assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigning_role_uses_highest_id_even_if_not_sorted() {
|
||||
let mut test = TestSetup::new();
|
||||
let deps = test.deps_mut();
|
||||
|
||||
let sorted = RoleAssignment {
|
||||
role: Role::EntryGateway,
|
||||
nodes: vec![1, 2, 3],
|
||||
};
|
||||
|
||||
let unsorted = RoleAssignment {
|
||||
role: Role::Layer1,
|
||||
nodes: vec![8, 5, 4],
|
||||
};
|
||||
|
||||
save_assignment(deps.storage, sorted).unwrap();
|
||||
save_assignment(deps.storage, unsorted).unwrap();
|
||||
|
||||
let storage = deps.as_ref().storage;
|
||||
|
||||
let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap();
|
||||
let inactive = active_bucket.other() as u8;
|
||||
let metadata = ROLES_METADATA.load(storage, inactive).unwrap();
|
||||
|
||||
assert_eq!(metadata.entry_gateway_metadata.highest_id, 3);
|
||||
assert_eq!(metadata.layer1_metadata.highest_id, 8);
|
||||
assert_eq!(metadata.highest_rewarded_id(), 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ pub use nym_explorer_api_requests::{
|
||||
|
||||
// Paths
|
||||
const API_VERSION: &str = "v1";
|
||||
const TMP: &str = "tmp";
|
||||
const UNSTABLE: &str = "unstable";
|
||||
const MIXNODES: &str = "mix-nodes";
|
||||
const GATEWAYS: &str = "gateways";
|
||||
|
||||
@@ -96,6 +98,13 @@ impl ExplorerClient {
|
||||
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
|
||||
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
|
||||
}
|
||||
|
||||
pub async fn unstable_get_gateways(
|
||||
&self,
|
||||
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
|
||||
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use log::{info, warn};
|
||||
use nym_explorer_api_requests::Location;
|
||||
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_task::TaskClient;
|
||||
|
||||
pub(crate) struct GeoLocateTask {
|
||||
@@ -25,6 +26,7 @@ impl GeoLocateTask {
|
||||
_ = interval_timer.tick() => {
|
||||
self.locate_mix_nodes().await;
|
||||
self.locate_gateways().await;
|
||||
self.locate_nym_nodes().await;
|
||||
}
|
||||
_ = self.shutdown.recv() => {
|
||||
trace!("Listener: Received shutdown");
|
||||
@@ -109,6 +111,83 @@ impl GeoLocateTask {
|
||||
trace!("All mix nodes located");
|
||||
}
|
||||
|
||||
async fn locate_nym_nodes(&mut self) {
|
||||
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
|
||||
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
|
||||
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
|
||||
|
||||
let geo_ip = self.state.inner.geo_ip.0.clone();
|
||||
|
||||
for (i, cache_item) in nym_nodes.values().enumerate() {
|
||||
if self
|
||||
.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.is_location_valid(cache_item.node_id())
|
||||
.await
|
||||
{
|
||||
// when the cached location is valid, don't locate and continue to next mix node
|
||||
continue;
|
||||
}
|
||||
|
||||
let bonded_host = &cache_item.bond_information.node.host;
|
||||
|
||||
match geo_ip.query(
|
||||
bonded_host,
|
||||
Some(
|
||||
cache_item
|
||||
.bond_information
|
||||
.node
|
||||
.custom_http_port
|
||||
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
|
||||
),
|
||||
) {
|
||||
Ok(opt) => match opt {
|
||||
Some(location) => {
|
||||
let location: Location = location.into();
|
||||
|
||||
trace!(
|
||||
"{} mix nodes already located. host {} is located in {:#?}",
|
||||
i,
|
||||
bonded_host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
if i > 0 && (i % 100) == 0 {
|
||||
info!("Located {} nym-nodes...", i + 1,);
|
||||
}
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.set_location(cache_item.node_id(), Some(location))
|
||||
.await;
|
||||
|
||||
// one node has been located, so return out of the loop
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
warn!("❌ Location for {bonded_host} not found.");
|
||||
self.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.set_location(cache_item.node_id(), None)
|
||||
.await;
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
// warn!(
|
||||
// "❌ Oh no! Location for {} failed. Error: {:#?}",
|
||||
// cache_item.mix_node().host,
|
||||
// e
|
||||
// );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
trace!("All nym-nodes nodes located");
|
||||
}
|
||||
|
||||
async fn locate_gateways(&mut self) {
|
||||
let gateways = self.state.inner.gateways.get_gateways().await;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes;
|
||||
use crate::http::swagger::get_docs;
|
||||
use crate::mix_node::http::mix_node_make_default_routes;
|
||||
use crate::mix_nodes::http::mix_nodes_make_default_routes;
|
||||
use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes;
|
||||
use crate::overview::http::overview_make_default_routes;
|
||||
use crate::ping::http::ping_make_default_routes;
|
||||
use crate::service_providers::http::service_providers_make_default_routes;
|
||||
@@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
|
||||
"/ping" => ping_make_default_routes(&openapi_settings),
|
||||
"/validators" => validators_make_default_routes(&openapi_settings),
|
||||
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
|
||||
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
|
||||
};
|
||||
|
||||
building_rocket
|
||||
|
||||
@@ -22,6 +22,7 @@ mod http;
|
||||
mod location;
|
||||
mod mix_node;
|
||||
pub(crate) mod mix_nodes;
|
||||
mod nym_nodes;
|
||||
mod overview;
|
||||
mod ping;
|
||||
pub(crate) mod service_providers;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
pub fn unstable_temp_nymnodes_make_default_routes(
|
||||
settings: &OpenApiSettings,
|
||||
) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![settings: all_gateways]
|
||||
}
|
||||
|
||||
#[openapi(tag = "UNSTABLE")]
|
||||
#[get("/gateways")]
|
||||
pub(crate) async fn all_gateways(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyDetailedGatewayBond>> {
|
||||
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
|
||||
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
|
||||
|
||||
Json(gateways)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
use crate::location::LocationCache;
|
||||
|
||||
pub(crate) type NymNodeLocationCache = LocationCache<NodeId>;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod location;
|
||||
pub(crate) mod models;
|
||||
|
||||
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200);
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::location::{LocationCache, LocationCacheItem};
|
||||
use crate::nym_nodes::location::NymNodeLocationCache;
|
||||
use crate::nym_nodes::CACHE_ENTRY_TTL;
|
||||
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
|
||||
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
|
||||
use nym_validator_client::models::NymNodeDescription;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
pub(crate) struct NymNodesCache {
|
||||
pub(crate) valid_until: SystemTime,
|
||||
pub(crate) bonded_nym_nodes: HashMap<NodeId, NymNodeDetails>,
|
||||
pub(crate) described_nodes: HashMap<NodeId, NymNodeDescription>,
|
||||
}
|
||||
|
||||
impl NymNodesCache {
|
||||
fn new() -> Self {
|
||||
NymNodesCache {
|
||||
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
|
||||
bonded_nym_nodes: Default::default(),
|
||||
described_nodes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// fn is_valid(&self) -> bool {
|
||||
// self.valid_until >= SystemTime::now()
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadSafeNymNodesCache {
|
||||
nymnodes: Arc<RwLock<NymNodesCache>>,
|
||||
locations: Arc<RwLock<LocationCache<NodeId>>>,
|
||||
}
|
||||
|
||||
impl ThreadSafeNymNodesCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
ThreadSafeNymNodesCache {
|
||||
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
|
||||
locations: Arc::new(RwLock::new(NymNodeLocationCache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self {
|
||||
ThreadSafeNymNodesCache {
|
||||
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
|
||||
locations: Arc::new(RwLock::new(locations)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool {
|
||||
self.locations
|
||||
.read()
|
||||
.await
|
||||
.get(&node_id)
|
||||
.map_or(false, |cache_item| {
|
||||
cache_item.valid_until > SystemTime::now()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_bonded_nymnodes(
|
||||
&self,
|
||||
) -> RwLockReadGuard<HashMap<NodeId, NymNodeDetails>> {
|
||||
let guard = self.nymnodes.read().await;
|
||||
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
|
||||
self.locations.read().await.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_location(&self, node_id: NodeId, location: Option<Location>) {
|
||||
// cache the location for this mix node so that it can be used when the mix node list is refreshed
|
||||
self.locations
|
||||
.write()
|
||||
.await
|
||||
.insert(node_id, LocationCacheItem::new_from_location(location));
|
||||
}
|
||||
|
||||
pub(crate) async fn update_cache(
|
||||
&self,
|
||||
all_bonds: Vec<NymNodeDetails>,
|
||||
descriptions: Vec<NymNodeDescription>,
|
||||
) {
|
||||
let mut guard = self.nymnodes.write().await;
|
||||
guard.bonded_nym_nodes = all_bonds
|
||||
.into_iter()
|
||||
.map(|details| (details.node_id(), details))
|
||||
.collect();
|
||||
guard.described_nodes = descriptions
|
||||
.into_iter()
|
||||
.map(|description| (description.node_id, description))
|
||||
.collect();
|
||||
|
||||
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
|
||||
}
|
||||
|
||||
pub(crate) async fn pretty_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
|
||||
let nodes_guard = self.nymnodes.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
let mut pretty_gateways = vec![];
|
||||
|
||||
for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes {
|
||||
let Some(description) = nodes_guard.described_nodes.get(node_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if description.description.declared_role.entry {
|
||||
let location = location_guard.get(node_id);
|
||||
let bond = &native_nymnode.bond_information;
|
||||
|
||||
pretty_gateways.push(PrettyDetailedGatewayBond {
|
||||
pledge_amount: bond.original_pledge.clone(),
|
||||
owner: bond.owner.clone(),
|
||||
block_height: bond.bonding_height,
|
||||
gateway: Gateway {
|
||||
host: bond.node.host.clone(),
|
||||
mix_port: description.description.mix_port(),
|
||||
clients_port: description.description.mixnet_websockets.ws_port,
|
||||
location: description
|
||||
.description
|
||||
.auxiliary_details
|
||||
.location
|
||||
.as_ref()
|
||||
.map(|l| l.to_string())
|
||||
.unwrap_or_default(),
|
||||
sphinx_key: description
|
||||
.description
|
||||
.host_information
|
||||
.keys
|
||||
.x25519
|
||||
.to_base58_string(),
|
||||
identity_key: bond.node.identity_key.clone(),
|
||||
version: description
|
||||
.description
|
||||
.build_information
|
||||
.build_version
|
||||
.clone(),
|
||||
},
|
||||
proxy: None,
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pretty_gateways
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache;
|
||||
use crate::mix_node::models::ThreadsafeMixNodeCache;
|
||||
use crate::mix_nodes::location::MixnodeLocationCache;
|
||||
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
|
||||
use crate::nym_nodes::location::NymNodeLocationCache;
|
||||
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
|
||||
use crate::ping::models::ThreadsafePingCache;
|
||||
use crate::validators::models::ThreadsafeValidatorCache;
|
||||
|
||||
@@ -30,6 +32,7 @@ pub struct ExplorerApiState {
|
||||
pub(crate) gateways: ThreadsafeGatewayCache,
|
||||
pub(crate) mixnode: ThreadsafeMixNodeCache,
|
||||
pub(crate) mixnodes: ThreadsafeMixNodesCache,
|
||||
pub(crate) nymnodes: ThreadSafeNymNodesCache,
|
||||
pub(crate) ping: ThreadsafePingCache,
|
||||
pub(crate) validators: ThreadsafeValidatorCache,
|
||||
pub(crate) geo_ip: ThreadsafeGeoIp,
|
||||
@@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk {
|
||||
pub(crate) country_node_distribution: CountryNodesDistribution,
|
||||
pub(crate) mixnode_location_cache: MixnodeLocationCache,
|
||||
pub(crate) gateway_location_cache: GatewayLocationCache,
|
||||
pub(crate) nymnode_location_cache: NymNodeLocationCache,
|
||||
pub(crate) as_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -85,6 +89,9 @@ impl ExplorerApiStateContext {
|
||||
mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(
|
||||
state.mixnode_location_cache,
|
||||
),
|
||||
nymnodes: ThreadSafeNymNodesCache::new_with_location_cache(
|
||||
state.nymnode_location_cache,
|
||||
),
|
||||
ping: ThreadsafePingCache::new(),
|
||||
validators: ThreadsafeValidatorCache::new(),
|
||||
validator_client: ThreadsafeValidatorClient::new(),
|
||||
@@ -101,6 +108,7 @@ impl ExplorerApiStateContext {
|
||||
gateways: ThreadsafeGatewayCache::new(),
|
||||
mixnode: ThreadsafeMixNodeCache::new(),
|
||||
mixnodes: ThreadsafeMixNodesCache::new(),
|
||||
nymnodes: ThreadSafeNymNodesCache::new(),
|
||||
ping: ThreadsafePingCache::new(),
|
||||
validators: ThreadsafeValidatorCache::new(),
|
||||
validator_client: ThreadsafeValidatorClient::new(),
|
||||
@@ -117,6 +125,7 @@ impl ExplorerApiStateContext {
|
||||
country_node_distribution: self.inner.country_node_distribution.get_all().await,
|
||||
mixnode_location_cache: self.inner.mixnodes.get_locations().await,
|
||||
gateway_location_cache: self.inner.gateways.get_locations().await,
|
||||
nymnode_location_cache: self.inner.nymnodes.get_locations().await,
|
||||
as_at: Utc::now(),
|
||||
};
|
||||
serde_json::to_writer(file, &state).expect("error writing state to disk");
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_mixnet_contract_common::GatewayBond;
|
||||
use crate::mix_nodes::CACHE_REFRESH_RATE;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
|
||||
use nym_task::TaskClient;
|
||||
use nym_validator_client::models::MixNodeBondAnnotated;
|
||||
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
|
||||
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
|
||||
use std::future::Future;
|
||||
|
||||
use crate::mix_nodes::CACHE_REFRESH_RATE;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
|
||||
pub(crate) struct ExplorerApiTasks {
|
||||
state: ExplorerApiStateContext,
|
||||
@@ -39,6 +39,28 @@ impl ExplorerApiTasks {
|
||||
bonds
|
||||
}
|
||||
|
||||
async fn retrieve_bonded_nymnodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||
info!("About to retrieve all nymnode bonds...");
|
||||
self.state
|
||||
.inner
|
||||
.validator_client
|
||||
.0
|
||||
.get_all_cached_bonded_nym_nodes()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retrieve_node_descriptions(
|
||||
&self,
|
||||
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
|
||||
info!("About to retrieve node descriptions...");
|
||||
self.state
|
||||
.inner
|
||||
.validator_client
|
||||
.0
|
||||
.get_all_cached_described_nodes()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retrieve_all_mixnodes(&self) -> Vec<MixNodeBondAnnotated> {
|
||||
info!("About to retrieve all mixnode bonds...");
|
||||
self.retrieve_mixnodes(
|
||||
@@ -130,10 +152,33 @@ impl ExplorerApiTasks {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_nymnodes_cache(&self) {
|
||||
let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| {
|
||||
error!("failed to retrieve nym node bonds: {err}");
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
let all_descriptions = self
|
||||
.retrieve_node_descriptions()
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
error!("failed to retrieve node descriptions: {err}");
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.update_cache(nym_node_bonds, all_descriptions)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning mix nodes task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
|
||||
interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
while !self.shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval_timer.tick() => {
|
||||
@@ -147,6 +192,10 @@ impl ExplorerApiTasks {
|
||||
|
||||
info!("Updating mix node cache...");
|
||||
self.update_mixnode_cache().await;
|
||||
|
||||
info!("Updating nymnode cache...");
|
||||
self.update_nymnodes_cache().await;
|
||||
info!("Done");
|
||||
}
|
||||
_ = self.shutdown.recv() => {
|
||||
trace!("Listener: Received shutdown");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,50 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Starting Docker:
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Stopping Docker:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev -- -p 8080
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:8080/](http://localhost:8080/) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,43 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
remark:
|
||||
# remove the next line in case you want to use this Docker Compose file separately
|
||||
# as otherwise it would complain for absence of Dockerfile
|
||||
build: .
|
||||
image: umputun/remark42:latest
|
||||
container_name: "remark42"
|
||||
hostname: "remark42"
|
||||
restart: always
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
# uncomment to expose directly (no proxy)
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "443:8443"
|
||||
|
||||
environment:
|
||||
- REMARK_URL=http://localhost:8081
|
||||
- SECRET=secret-key
|
||||
- AUTH_ANON=true
|
||||
- SITE=remark42
|
||||
# - DEBUG=true
|
||||
# - AUTH_GOOGLE_CID
|
||||
# - AUTH_GOOGLE_CSEC
|
||||
# - AUTH_GITHUB_CID
|
||||
# - AUTH_GITHUB_CSEC
|
||||
# - AUTH_FACEBOOK_CID
|
||||
# - AUTH_FACEBOOK_CSEC
|
||||
# - AUTH_DISQUS_CID
|
||||
# - AUTH_DISQUS_CSEC
|
||||
# Enable it only for the initial comment import or for manual backups.
|
||||
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
|
||||
# to keep creating backups manually!
|
||||
- ADMIN_PASSWD=password
|
||||
volumes:
|
||||
- ./var:/srv/var
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "remark42-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.0.0-rc-02c0e824-20241028",
|
||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||
"next": "15.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
.page {
|
||||
--gray-rgb: 0, 0, 0;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 20px 1fr 20px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
min-height: 100svh;
|
||||
padding: 80px;
|
||||
gap: 64px;
|
||||
font-family: var(--font-geist-sans);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page {
|
||||
--gray-rgb: 255, 255, 255;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
font-family: var(--font-geist-mono);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.main li:not(:last-of-type) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.main code {
|
||||
font-family: inherit;
|
||||
background: var(--gray-alpha-100);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
maxWidth={"60%"}
|
||||
margin={"50px auto"}
|
||||
>
|
||||
<div>NymNode X</div>
|
||||
<div>Info about NymNode X</div>
|
||||
<div id="remark42"></div>
|
||||
|
||||
{/* Configuration Script */}
|
||||
<Script
|
||||
id="remark-config"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var remark_config = {
|
||||
host: 'http://localhost:8081', // Updated to match the REMARK_URL
|
||||
site_id: 'remark42',
|
||||
components: ['embed', 'last-comments'],
|
||||
max_shown_comments: 100,
|
||||
theme: 'light',
|
||||
page_title: 'My custom title for a page',
|
||||
locale: 'en',
|
||||
show_email_subscription: false,
|
||||
simple_view: true,
|
||||
no_footer: true
|
||||
};
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Initialization Script */}
|
||||
<Script
|
||||
id="remark-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 992 B |
|
After Width: | Height: | Size: 963 B |
@@ -0,0 +1,190 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
EXPLORER_API,
|
||||
COSMOS_API,
|
||||
VALIDATOR_API_EPOCH,
|
||||
VALIDATOR_API_SUPPLY,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_BASE,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
} from "../urls";
|
||||
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: any;
|
||||
nymNodesData: any;
|
||||
packetsAndStakingData: any;
|
||||
currentEpochData: any;
|
||||
currentEpochRewardsData: any;
|
||||
}
|
||||
|
||||
export interface ExplorerCache {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Extend the global object with our custom property
|
||||
var explorerCache: ExplorerCache | undefined;
|
||||
}
|
||||
|
||||
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH
|
||||
const fetchCurrentEpoch = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH REWARDS
|
||||
const fetchCurrentEpochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CIRCULATING NYM SUPPLY
|
||||
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH PACKETS AND STAKING
|
||||
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
currentEpochRes,
|
||||
currentEpochRewardsRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchCurrentEpoch,
|
||||
fetchCurrentEpochRewards,
|
||||
]);
|
||||
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
currentEpochRes.json(),
|
||||
currentEpochRewardsRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
// makes sure the cache exists in global memory
|
||||
let doUpdate = false;
|
||||
const now = new Date();
|
||||
if (!global.explorerCache) {
|
||||
global.explorerCache = {};
|
||||
doUpdate = true;
|
||||
}
|
||||
if (
|
||||
global.explorerCache.lastUpdated &&
|
||||
now.getDate() - global.explorerCache.lastUpdated.getDate() >
|
||||
CACHE_TIME_SECONDS
|
||||
) {
|
||||
doUpdate = true;
|
||||
}
|
||||
|
||||
// if the cache has expired or never existed, get it from API's
|
||||
if (doUpdate) {
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await getExplorerData();
|
||||
|
||||
packetsAndStakingData.pop();
|
||||
|
||||
global.explorerCache.data = {
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
};
|
||||
global.explorerCache.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheExplorerData() {
|
||||
await ensureCacheExists();
|
||||
|
||||
if (!global.explorerCache?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return global.explorerCache.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom API route that returns metadata from Strapi about images: height, width, strapi download url.
|
||||
*
|
||||
* The response from Strapi is cached in memory for CACHE_TIME_SECONDS.
|
||||
*/
|
||||
// export default async function handler(
|
||||
// req: NextApiRequest,
|
||||
// res: NextApiResponse
|
||||
// ) {
|
||||
// // return cached data
|
||||
// const data = await getCacheExplorerData();
|
||||
// if (data) {
|
||||
// res.status(200).json(data);
|
||||
// res.end();
|
||||
// }
|
||||
|
||||
// // catch-all
|
||||
// res.status(404).end();
|
||||
// }
|
||||
@@ -0,0 +1,18 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const COSMOS_API =
|
||||
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
|
||||
export const VALIDATOR_API_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
|
||||
export const CURRENT_EPOCH =
|
||||
" https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
@@ -0,0 +1,307 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { Card, CardContent } from "@mui/material";
|
||||
import { ExplorerStaticProgressBar } from "./ExplorerStaticProgressBar";
|
||||
import { MultiSegmentProgressBar } from "./ExplorerMultiSegmentProgressBar";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
|
||||
export interface IAccontStatsRowProps {
|
||||
type: string;
|
||||
allocation: number;
|
||||
amount: number;
|
||||
value: number;
|
||||
history?: { type: string; amount: number }[];
|
||||
isLastRow?: boolean;
|
||||
progressBarColor?: string;
|
||||
}
|
||||
|
||||
const progressBarColours = [
|
||||
"#BEF885",
|
||||
"#7FB0FF",
|
||||
"#00D17D",
|
||||
"#004650",
|
||||
"#FEECB3",
|
||||
];
|
||||
|
||||
const TABLET_WIDTH = "(min-width:700px)";
|
||||
|
||||
const Row = (props: IAccontStatsRowProps) => {
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
|
||||
const {
|
||||
type,
|
||||
allocation,
|
||||
amount,
|
||||
value,
|
||||
history,
|
||||
isLastRow,
|
||||
progressBarColor,
|
||||
} = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Main Row */}
|
||||
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Typography>{type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography>{allocation}%</Typography>
|
||||
<ExplorerStaticProgressBar
|
||||
value={allocation}
|
||||
color={progressBarColor || "green"}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
{amount} NYM
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
$ {value}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
// MOBILE VIEW
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<CircleIcon sx={{ color: progressBarColor }} fontSize="small" />
|
||||
{type}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Typography>{amount} NYM</Typography>
|
||||
<Typography>$ {value}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* History Rows */}
|
||||
{history &&
|
||||
open &&
|
||||
history.map((historyRow, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
pl: 5,
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
<span style={{ marginRight: 8 }}>•</span>
|
||||
{historyRow.type}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{historyRow.amount}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{/* Any additional content */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccountStatsCardProps {
|
||||
rows: Array<IAccontStatsRowProps>;
|
||||
overTitle?: string;
|
||||
priceTitle?: number;
|
||||
}
|
||||
|
||||
export const AccountStatsCard = (props: IAccountStatsCardProps) => {
|
||||
const { rows, overTitle, priceTitle } = props;
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
const progressBarPercentages = () => {
|
||||
return rows.map((row, i) => row.allocation);
|
||||
};
|
||||
const getProgressValues = () => {
|
||||
const percentages = progressBarPercentages();
|
||||
const result: Array<{ percentage: number; color: string }> = [];
|
||||
percentages.map((value, i) => {
|
||||
result.push({
|
||||
percentage: value,
|
||||
color: progressBarColours[i],
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const progressValues = getProgressValues();
|
||||
|
||||
return (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{priceTitle && (
|
||||
<Typography fontSize={24} mb={3}>
|
||||
${priceTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Amount</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Value</Typography>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={i}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,391 @@
|
||||
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
|
||||
import React, { FC, ReactElement, ReactEventHandler, useEffect } from "react";
|
||||
import { ExplorerLineChart, IExplorerLineChartData } from "./ExplorerLineChart";
|
||||
import {
|
||||
ExplorerDynamicProgressBar,
|
||||
IExplorerDynamicProgressBarProps,
|
||||
} from "./ExplorerDynamicProgressBar";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import { NymTokenSVG } from "../icons/NymTokenSVG";
|
||||
import { CopyToClipboard } from "@nymproject/react/clipboard/CopyToClipboard";
|
||||
import Image from "next/image";
|
||||
import profileImagePlaceholder from "../../public/profileImagePlaceholder.png";
|
||||
import Flag from "react-world-flags";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import Script from "next/script";
|
||||
import { useMainContext } from "../context/main";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
remark_config: {
|
||||
host: string;
|
||||
site_id: string;
|
||||
components: string[];
|
||||
max_shown_comments: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
show_email_subscription: boolean;
|
||||
simple_view: boolean;
|
||||
no_footer: boolean;
|
||||
};
|
||||
REMARK42: {
|
||||
createInstance: (config: typeof window.remark_config) => void;
|
||||
changeTheme: (theme: "light" | "dark") => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps
|
||||
): ReactElement => {
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box mb={3} display={"flex"}>
|
||||
{numberWentUp ? (
|
||||
<ArrowUpwardIcon sx={{ color: "#00CA33" }} fontSize="small" />
|
||||
) : (
|
||||
<ArrowDownwardIcon sx={{ color: "#DF1400" }} fontSize="small" />
|
||||
)}
|
||||
<Typography sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}>
|
||||
{percentage}% (24H)
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardTitlePriceProps {
|
||||
price: number;
|
||||
upDownLine: ICardUpDownPriceLineProps;
|
||||
}
|
||||
const CardTitlePrice = (props: ICardTitlePriceProps): React.ReactNode => {
|
||||
const { price, upDownLine } = props;
|
||||
return (
|
||||
<Box display={"flex"} flexDirection={"column"} alignItems={"flex-end"}>
|
||||
<Box display={"flex"} justifyContent={"space-between"} width={"100%"}>
|
||||
<Box display={"flex"} gap={1}>
|
||||
<NymTokenSVG />
|
||||
<Typography>NYM</Typography>
|
||||
</Box>
|
||||
<Typography>${price}</Typography>
|
||||
</Box>
|
||||
<CardUpDownPriceLine {...upDownLine} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ICardDataRowsProps {
|
||||
rows: Array<{ key: string; value: string }>;
|
||||
}
|
||||
export const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
|
||||
const { rows } = props;
|
||||
|
||||
return (
|
||||
<Box mb={3}>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i === 0 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography>{row.key}</Typography>
|
||||
<Typography>{row.value}</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
interface ICardProileImage {
|
||||
url?: string;
|
||||
}
|
||||
const CardProfileImage = (props: ICardProileImage) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} mb={3}>
|
||||
{url ? (
|
||||
<Image src={url} alt="linkedIn" width={80} height={80} />
|
||||
) : (
|
||||
<Image
|
||||
src={profileImagePlaceholder}
|
||||
alt="linkedIn"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardProfileCountry {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
}
|
||||
|
||||
const CardProfileCountry = (props: ICardProfileCountry) => {
|
||||
const { countryCode, countryName } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} gap={2} mb={3}>
|
||||
<Flag code={countryCode} width="20" />
|
||||
<Typography textTransform={"uppercase"}>{countryName}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardCopyAddressProps {
|
||||
title: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const CardCopyAddress = (props: ICardCopyAddressProps) => {
|
||||
const { title, address } = props;
|
||||
return (
|
||||
<Box
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={2}
|
||||
borderBottom={"1px solid #C3D7D7"}
|
||||
>
|
||||
<Typography textTransform={"uppercase"}>{title}</Typography>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography>{address}</Typography>
|
||||
|
||||
<CopyToClipboard
|
||||
sx={{ mr: 0.5, color: "grey.400" }}
|
||||
smallIcons
|
||||
value={address}
|
||||
tooltip={`Copy identity key ${address} to clipboard`}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardQRCodeProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CardQRCode = (props: ICardQRCodeProps) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<Box
|
||||
padding={2}
|
||||
border={"1px solid #C3D7D7"}
|
||||
mb={3}
|
||||
display={"block"}
|
||||
width={"unset"}
|
||||
>
|
||||
<QRCodeCanvas value={url} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardRatingsProps {
|
||||
ratings: Array<{ title: string; numberOfStars: number }>;
|
||||
}
|
||||
|
||||
const CardRatings = (props: ICardRatingsProps) => {
|
||||
const { ratings } = props;
|
||||
|
||||
return (
|
||||
<Box mb={3}>
|
||||
{ratings.map((rating, i) => {
|
||||
const Stars = () => {
|
||||
const stars = [];
|
||||
for (let i = 0; i < rating.numberOfStars; i++) {
|
||||
stars.push(<StarIcon sx={{ color: "#14E76F" }} fontSize="small" />);
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
const RatingTitle = () => {
|
||||
if (rating.numberOfStars === 1) {
|
||||
return <Typography>Bad</Typography>;
|
||||
} else if (rating.numberOfStars === 2) {
|
||||
return <Typography>Bad</Typography>;
|
||||
} else if (rating.numberOfStars === 3) {
|
||||
return <Typography>ok</Typography>;
|
||||
} else if (rating.numberOfStars === 4) {
|
||||
return <Typography>Good</Typography>;
|
||||
} else {
|
||||
return <Typography>Excellent</Typography>;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i < ratings.length - 1 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography>{rating.title}</Typography>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<Stars />
|
||||
<RatingTitle />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CardChat = () => {
|
||||
const { mode } = useMainContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
// Set Remark42 configuration on the window object
|
||||
window.remark_config = {
|
||||
host: "https://remark.blockfend.com",
|
||||
site_id: "nym-explorer-test",
|
||||
components: ["embed", "last-comments"],
|
||||
max_shown_comments: 100,
|
||||
theme: mode === "light" ? "light" : "dark",
|
||||
locale: "en",
|
||||
show_email_subscription: false,
|
||||
simple_view: true,
|
||||
no_footer: true,
|
||||
};
|
||||
|
||||
// Dynamically load the Remark42 script if it doesn't exist
|
||||
if (!document.getElementById("remark42-script")) {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${window.remark_config.host}/web/embed.js`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.id = "remark42-script";
|
||||
document.body.appendChild(script);
|
||||
} else if (window.REMARK42) {
|
||||
// Re-initialize if the script is already loaded
|
||||
window.REMARK42.createInstance(window.remark_config);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// React to mode changes and update Remark42 theme
|
||||
useEffect(() => {
|
||||
if (window.REMARK42 && window.REMARK42.changeTheme) {
|
||||
window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div id="remark42" className="remark"></div>
|
||||
<Script
|
||||
id="remark-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (window.REMARK42) {
|
||||
window.REMARK42.createInstance(window.remark_config);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
profileImage?: ICardProileImage;
|
||||
title?: string | number;
|
||||
profileCountry?: ICardProfileCountry;
|
||||
upDownLine?: ICardUpDownPriceLineProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
graph?: { data: Array<IExplorerLineChartData>; color: string; label: string };
|
||||
progressBar?: IExplorerDynamicProgressBarProps;
|
||||
paragraph?: string;
|
||||
onClick?: ReactEventHandler;
|
||||
nymAddress?: ICardCopyAddressProps;
|
||||
identityKey?: ICardCopyAddressProps;
|
||||
qrCode?: ICardQRCodeProps;
|
||||
ratings?: ICardRatingsProps;
|
||||
chat?: boolean;
|
||||
button?: {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const ExplorerCard: FC<ContentCardProps> = ({
|
||||
title,
|
||||
titlePrice,
|
||||
overTitle,
|
||||
upDownLine,
|
||||
dataRows,
|
||||
graph,
|
||||
progressBar,
|
||||
paragraph,
|
||||
onClick,
|
||||
profileImage,
|
||||
profileCountry,
|
||||
nymAddress,
|
||||
identityKey,
|
||||
qrCode,
|
||||
ratings,
|
||||
chat,
|
||||
button,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{profileImage && <CardProfileImage {...profileImage} />}
|
||||
{title && (
|
||||
<Typography fontSize={24} mb={3}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{profileCountry && <CardProfileCountry {...profileCountry} />}
|
||||
{upDownLine && <CardUpDownPriceLine {...upDownLine} />}
|
||||
{titlePrice && <CardTitlePrice {...titlePrice} />}
|
||||
{qrCode && <CardQRCode {...qrCode} />}
|
||||
{nymAddress && <CardCopyAddress {...nymAddress} />}
|
||||
{identityKey && <CardCopyAddress {...identityKey} />}
|
||||
{dataRows && <CardDataRows {...dataRows} />}
|
||||
{ratings && <CardRatings {...ratings} />}
|
||||
{graph && (
|
||||
<Box mb={3}>
|
||||
<ExplorerLineChart
|
||||
data={graph.data}
|
||||
color={graph.color}
|
||||
label={graph.label}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{progressBar && (
|
||||
<Box mb={3}>
|
||||
<ExplorerDynamicProgressBar {...progressBar} />
|
||||
</Box>
|
||||
)}
|
||||
{paragraph && <Typography>{paragraph}</Typography>}
|
||||
{chat && <CardChat />}
|
||||
{button && (
|
||||
<Button onClick={button.onClick} variant="contained">
|
||||
{button.label}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
export interface IExplorerDynamicProgressBarProps {
|
||||
title?: string;
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
export const ExplorerDynamicProgressBar = (
|
||||
props: IExplorerDynamicProgressBarProps
|
||||
) => {
|
||||
const { start, showEpoch, title } = props;
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate start timestamp
|
||||
if (isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to calculate progress
|
||||
const calculateProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime < startTime) {
|
||||
return 0;
|
||||
}
|
||||
if (currentTime >= endTime) {
|
||||
return 100;
|
||||
}
|
||||
const elapsed = currentTime - startTime;
|
||||
const total = endTime - startTime;
|
||||
return (elapsed / total) * 100;
|
||||
};
|
||||
|
||||
// Set initial progress and start timer
|
||||
setProgress(calculateProgress());
|
||||
const timer = setInterval(() => {
|
||||
setProgress(calculateProgress());
|
||||
}, 60000); // Update every minute (60000 milliseconds)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based
|
||||
const year = date.getFullYear();
|
||||
return `${hours}:${minutes}, ${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{title && (
|
||||
<Typography mb={2} textTransform={"uppercase"}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#14E76F",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showEpoch && (
|
||||
<Box mt={2}>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>START:</Typography>
|
||||
<Typography> {startTime ? formatDate(startTime) : ""}</Typography>
|
||||
</Box>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>END:</Typography>
|
||||
<Typography> {endTime ? formatDate(endTime) : ""}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
import Loading from "./Loading";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const LineChart = dynamic(
|
||||
() => import("@nivo/line").then((m) => m.ResponsiveLine),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export interface IExplorerLineChartData {
|
||||
date_utc: string;
|
||||
numericData?: number;
|
||||
// purpleLineNumericData?: number;
|
||||
}
|
||||
|
||||
interface IAxes {
|
||||
x: Date;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ILineAxes {
|
||||
id: string;
|
||||
data: Array<IAxes>;
|
||||
}
|
||||
|
||||
export const ExplorerLineChart = ({
|
||||
data,
|
||||
color,
|
||||
label,
|
||||
}: {
|
||||
data: Array<IExplorerLineChartData>;
|
||||
color: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("lg"));
|
||||
|
||||
const [chartData, setChartData] = useState<Array<ILineAxes>>();
|
||||
|
||||
useEffect(() => {
|
||||
const resultData = transformData(data);
|
||||
if (resultData.length > 0) {
|
||||
setChartData(resultData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const transformData = (data: Array<IExplorerLineChartData>) => {
|
||||
const lineData: ILineAxes = {
|
||||
id: label,
|
||||
data: [],
|
||||
};
|
||||
|
||||
// const purpleLineData: ILineAxes = {
|
||||
// id: "Numeric Data 2",
|
||||
// data: [],
|
||||
// };
|
||||
|
||||
data.map((item: any) => {
|
||||
const axesGreenLineData: IAxes = {
|
||||
x: new Date(item.date_utc),
|
||||
y: item.numericData,
|
||||
};
|
||||
|
||||
lineData.data.push(axesGreenLineData);
|
||||
|
||||
// const axesPurpleLineData: IAxes = {
|
||||
// x: new Date(item.date_utc),
|
||||
// y: item.purpleLineNumericData,
|
||||
// };
|
||||
|
||||
// purpleLineData.data.push(axesPurpleLineData);
|
||||
});
|
||||
return [{ ...lineData }];
|
||||
};
|
||||
|
||||
const yformat = (num: number | string | Date) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B";
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
||||
}
|
||||
return num;
|
||||
} else {
|
||||
throw new Error("Unexpected value");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width={"100%"} height={isDesktop ? 200 : 150}>
|
||||
{chartData && (
|
||||
<LineChart
|
||||
curve="monotoneX"
|
||||
colors={[color]}
|
||||
data={chartData}
|
||||
animate
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 36,
|
||||
right: 16,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
grid: { line: { strokeWidth: 0 } },
|
||||
tooltip: { container: { color: "black" } },
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
|
||||
},
|
||||
ticks: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
}}
|
||||
yScale={{ min: 1, type: "linear" }}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
axisLeft={{
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
legendOffset: -12,
|
||||
tickValues:
|
||||
chartData[0].data.length > 7 ? "every 4 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export interface MultiSegmentProgressBarProps {
|
||||
values: { percentage: number; color: string }[]; // Array of percentage and color pairs
|
||||
height?: number; // Optional height, default is 8
|
||||
borderRadius?: number; // Optional border radius, default is 4
|
||||
backgroundColor?: string; // Optional background color for the bar, default is light gray
|
||||
}
|
||||
|
||||
export const MultiSegmentProgressBar: React.FC<
|
||||
MultiSegmentProgressBarProps
|
||||
> = ({ values, height = 8, borderRadius = 4, backgroundColor = "#CAD6D7" }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height,
|
||||
borderRadius,
|
||||
overflow: "hidden",
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{values.map((value, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
width: `${value.percentage}%`,
|
||||
backgroundColor: value.color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
|
||||
export interface IExplorerStaticProgressBarProps {
|
||||
color: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const ExplorerStaticProgressBar = (
|
||||
props: IExplorerStaticProgressBarProps
|
||||
) => {
|
||||
const { color, value } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={value}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: color,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
|
||||
interface TwoSidedSwitchProps {
|
||||
leftLabel: string; // Label for the left side
|
||||
rightLabel: string; // Label for the right side
|
||||
onSwitch?: (side: "left" | "right") => void; // Callback when switched
|
||||
}
|
||||
|
||||
const TwoSidedSwitch: React.FC<TwoSidedSwitchProps> = ({
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
onSwitch,
|
||||
}) => {
|
||||
const [selectedSide, setSelectedSide] = useState<"left" | "right">("left");
|
||||
|
||||
const handleSwitch = (side: "left" | "right") => {
|
||||
setSelectedSide(side);
|
||||
if (onSwitch) onSwitch(side);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
width: "200px",
|
||||
height: "40px",
|
||||
border: "2px solid black",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleSwitch("left")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "left" ? "black" : "white",
|
||||
color: selectedSide === "left" ? "white" : "black",
|
||||
borderRadius: 0,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedSide === "left" ? "black" : "lightgray",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{leftLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitch("right")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "right" ? "black" : "white",
|
||||
color: selectedSide === "right" ? "white" : "black",
|
||||
borderRadius: 0,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedSide === "right" ? "black" : "lightgray",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{rightLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoSidedSwitch;
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginY: 10,
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
|
||||
import { ICardDataRowsProps } from "./ExplorerCard";
|
||||
|
||||
export interface IStakingCardProps {
|
||||
title?: string;
|
||||
titleCenter?: string;
|
||||
paragraphCenter?: string;
|
||||
closeButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
addressInput?: boolean;
|
||||
amountInput?: boolean;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
blockExplorerLink?: string;
|
||||
backButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
nextButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
}
|
||||
export const StakingCard = (props: IStakingCardProps) => {
|
||||
const {
|
||||
title,
|
||||
titleCenter,
|
||||
closeButton,
|
||||
paragraphCenter,
|
||||
addressInput,
|
||||
amountInput,
|
||||
dataRows,
|
||||
blockExplorerLink,
|
||||
backButton,
|
||||
nextButton,
|
||||
} = props;
|
||||
return (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent></CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const NymTokenSVG: FCWithChildren = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_7134_15888)">
|
||||
<path
|
||||
d="M17.07 3.43C13.17 -0.480002 6.83 -0.480002 2.93 3.43C-0.980002 7.34 -0.980002 13.67 2.93 17.57C6.84 21.48 13.17 21.48 17.07 17.57C20.98 13.67 20.98 7.33 17.07 3.43ZM16.21 16.71C12.78 20.14 7.21 20.14 3.78 16.71C0.349997 13.28 0.349997 7.71 3.78 4.28C7.21 0.849997 12.78 0.849997 16.21 4.28C19.65 7.72 19.65 13.28 16.21 16.71Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M15.4 16.33V4.66999C14.89 4.18999 14.32 3.76999 13.71 3.43999V14.59L6.35001 3.39999C5.71001 3.73999 5.12001 4.15999 4.60001 4.65999V16.33C5.11001 16.81 5.68001 17.23 6.29001 17.56V6.40999L13.65 17.6C14.29 17.26 14.88 16.83 15.4 16.33Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7134_15888">
|
||||
<rect
|
||||
width="20"
|
||||
height="20"
|
||||
fill={color}
|
||||
transform="translate(0 0.5)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
import '@interchain-ui/react/styles'
|
||||
import { App } from './App'
|
||||
import type { Metadata } from "next";
|
||||
import "@interchain-ui/react/styles";
|
||||
import { App } from "./App";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Nym Network Explorer',
|
||||
}
|
||||
title: "Nym Network Explorer",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
@@ -17,5 +17,5 @@ export default function RootLayout({
|
||||
<App>{children}</App>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { GatewayBond } from '@/app/typeDefs/explorer-api'
|
||||
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
|
||||
import * as React from "react";
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material";
|
||||
import { useParams } from "next/navigation";
|
||||
import { GatewayBond } from "@/app/typeDefs/explorer-api";
|
||||
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
|
||||
import {
|
||||
gatewayEnrichedToGridRow,
|
||||
GatewayEnrichedRowType,
|
||||
} from '@/app/components/Gateways/Gateways'
|
||||
import { ComponentError } from '@/app/components/ComponentError'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
|
||||
import { UptimeChart } from '@/app/components/UptimeChart'
|
||||
} from "@/app/components/Gateways/Gateways";
|
||||
import { ComponentError } from "@/app/components/ComponentError";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
|
||||
import { UptimeChart } from "@/app/components/UptimeChart";
|
||||
import {
|
||||
GatewayContextProvider,
|
||||
useGatewayContext,
|
||||
} from '@/app/context/gateway'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import { Title } from '@/app/components/Title'
|
||||
} from "@/app/context/gateway";
|
||||
import { useMainContext } from "@/app/context/main";
|
||||
import { Title } from "@/app/components/Title";
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'identity_key',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
field: "identity_key",
|
||||
title: "Identity Key",
|
||||
headerAlign: "left",
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
headerAlign: 'left',
|
||||
field: "bond",
|
||||
title: "Bond",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'node_performance',
|
||||
title: 'Routing Score',
|
||||
headerAlign: 'left',
|
||||
field: "node_performance",
|
||||
title: "Routing Score",
|
||||
headerAlign: "left",
|
||||
tooltipInfo:
|
||||
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
headerAlign: 'left',
|
||||
field: "avgUptime",
|
||||
title: "Avg. Score",
|
||||
headerAlign: "left",
|
||||
tooltipInfo: "Gateway's average routing score in the last 24 hours",
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'IP',
|
||||
headerAlign: 'left',
|
||||
field: "host",
|
||||
title: "IP",
|
||||
headerAlign: "left",
|
||||
width: 99,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
field: "location",
|
||||
title: "Location",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
field: "owner",
|
||||
title: "Owner",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: 'Version',
|
||||
headerAlign: 'left',
|
||||
field: "version",
|
||||
title: "Version",
|
||||
headerAlign: "left",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows gateway details
|
||||
@@ -74,26 +74,26 @@ const columns: ColumnsType[] = [
|
||||
const PageGatewayDetailsWithState = ({
|
||||
selectedGateway,
|
||||
}: {
|
||||
selectedGateway: GatewayBond | undefined
|
||||
selectedGateway: GatewayBond | undefined;
|
||||
}) => {
|
||||
const [enrichGateway, setEnrichGateway] =
|
||||
React.useState<GatewayEnrichedRowType>()
|
||||
const [status, setStatus] = React.useState<number[] | undefined>()
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext()
|
||||
React.useState<GatewayEnrichedRowType>();
|
||||
const [status, setStatus] = React.useState<number[] | undefined>();
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uptimeReport?.data && selectedGateway) {
|
||||
setEnrichGateway(
|
||||
gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [uptimeReport, selectedGateway])
|
||||
}, [uptimeReport, selectedGateway]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enrichGateway) {
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort])
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
|
||||
}
|
||||
}, [enrichGateway])
|
||||
}, [enrichGateway]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
@@ -115,7 +115,7 @@ const PageGatewayDetailsWithState = ({
|
||||
<ContentCard title="Gateway Status">
|
||||
<TwoColSmallTable
|
||||
loading={false}
|
||||
keys={['Mix port', 'Client WS API Port']}
|
||||
keys={["Mix port", "Client WS API Port"]}
|
||||
values={status.map((each) => each)}
|
||||
icons={status.map((elem) => !!elem)}
|
||||
/>
|
||||
@@ -137,39 +137,40 @@ const PageGatewayDetailsWithState = ({
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}></Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loadingW and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>()
|
||||
const { gateways } = useMainContext()
|
||||
const { id } = useParams()
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(
|
||||
gateways.data.find((g) => g.gateway.identity_key === id)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [gateways, id])
|
||||
}, [gateways, id]);
|
||||
|
||||
if (gateways?.isLoading) {
|
||||
return <CircularProgress />
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (gateways?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(gateways?.error)
|
||||
console.error(gateways?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
Oh no! Could not load mixnode <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
@@ -177,31 +178,31 @@ const PageGatewayDetailGuard = () => {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Gateway not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />
|
||||
}
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
const PageGatewayDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { id } = useParams();
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
if (!id || typeof id !== "string") {
|
||||
return (
|
||||
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GatewayContextProvider gatewayIdentityKey={id}>
|
||||
<PageGatewayDetailGuard />
|
||||
</GatewayContextProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PageGatewayDetail
|
||||
export default PageGatewayDetail;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
@@ -8,75 +8,82 @@ import {
|
||||
CircularProgress,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
|
||||
import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown'
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
|
||||
import { BondBreakdownTable } from "@/app/components/MixNodes/BondBreakdown";
|
||||
import {
|
||||
DelegatorsInfoTable,
|
||||
EconomicsInfoColumns,
|
||||
EconomicsInfoRows,
|
||||
} from '@/app/components/MixNodes/Economics'
|
||||
import { ComponentError } from '@/app/components/ComponentError'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
|
||||
import { UptimeChart } from '@/app/components/UptimeChart'
|
||||
import { WorldMap } from '@/app/components/WorldMap'
|
||||
import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection'
|
||||
} from "@/app/components/MixNodes/Economics";
|
||||
import { ComponentError } from "@/app/components/ComponentError";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
|
||||
import { UptimeChart } from "@/app/components/UptimeChart";
|
||||
import { WorldMap } from "@/app/components/WorldMap";
|
||||
import { MixNodeDetailSection } from "@/app/components/MixNodes/DetailSection";
|
||||
import {
|
||||
MixnodeContextProvider,
|
||||
useMixnodeContext,
|
||||
} from '@/app/context/mixnode'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { useParams } from 'next/navigation'
|
||||
} from "@/app/context/mixnode";
|
||||
import { Title } from "@/app/components/Title";
|
||||
import { useIsMobile } from "@/app/hooks/useIsMobile";
|
||||
import { useParams } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
import Head from "next/head";
|
||||
import { useEffect } from "react";
|
||||
import { useMainContext } from "@/app/context/main";
|
||||
import { ExplorerCard } from "@/app/components/ExplorerCard";
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
width: '15%',
|
||||
field: "owner",
|
||||
title: "Owner",
|
||||
width: "15%",
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
title: 'Identity Key',
|
||||
width: '15%',
|
||||
field: "identity_key",
|
||||
title: "Identity Key",
|
||||
width: "15%",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Stake',
|
||||
width: '12.5%',
|
||||
field: "bond",
|
||||
title: "Stake",
|
||||
width: "12.5%",
|
||||
},
|
||||
{
|
||||
field: 'stake_saturation',
|
||||
title: 'Stake Saturation',
|
||||
width: '12.5%',
|
||||
field: "stake_saturation",
|
||||
title: "Stake Saturation",
|
||||
width: "12.5%",
|
||||
tooltipInfo:
|
||||
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
|
||||
"Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.",
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
width: '10%',
|
||||
title: 'Bond %',
|
||||
field: "self_percentage",
|
||||
width: "10%",
|
||||
title: "Bond %",
|
||||
tooltipInfo:
|
||||
"Percentage of the operator's bond to the total stake on the node",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'host',
|
||||
width: '10%',
|
||||
title: 'Host',
|
||||
field: "host",
|
||||
width: "10%",
|
||||
title: "Host",
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
field: "location",
|
||||
title: "Location",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'layer',
|
||||
title: 'Layer',
|
||||
field: "layer",
|
||||
title: "Layer",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows mix node details
|
||||
@@ -90,10 +97,53 @@ const PageMixnodeDetailWithState = () => {
|
||||
status,
|
||||
uptimeStory,
|
||||
uniqDelegations,
|
||||
} = useMixnodeContext()
|
||||
const isMobile = useIsMobile()
|
||||
} = useMixnodeContext();
|
||||
const isMobile = useIsMobile();
|
||||
const { mode } = useMainContext();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (typeof window !== "undefined") {
|
||||
// // Set Remark42 configuration on the window object
|
||||
// window.remark_config = {
|
||||
// host: "http://localhost:8081",
|
||||
// site_id: "remark42",
|
||||
// components: ["embed", "last-comments"],
|
||||
// max_shown_comments: 100,
|
||||
// theme: mode === "light" ? "light" : "dark",
|
||||
// page_title: "My custom title for a page",
|
||||
// locale: "en",
|
||||
// show_email_subscription: false,
|
||||
// simple_view: true,
|
||||
// no_footer: true,
|
||||
// };
|
||||
|
||||
// // Dynamically load the Remark42 script if it doesn't exist
|
||||
// if (!document.getElementById("remark42-script")) {
|
||||
// const script = document.createElement("script");
|
||||
// script.src = `${window.remark_config.host}/web/embed.js`;
|
||||
// script.async = true;
|
||||
// script.defer = true;
|
||||
// script.id = "remark42-script";
|
||||
// document.body.appendChild(script);
|
||||
// } else if (window.REMARK42) {
|
||||
// // Re-initialize if the script is already loaded
|
||||
// window.REMARK42.createInstance(window.remark_config);
|
||||
// }
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// // React to mode changes and update Remark42 theme
|
||||
// useEffect(() => {
|
||||
// if (window.REMARK42 && window.REMARK42.changeTheme) {
|
||||
// window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
|
||||
// }
|
||||
// }, [mode]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Head>
|
||||
<title>Mixnode Detail</title>
|
||||
</Head>
|
||||
<Title text="Mixnode Detail" />
|
||||
<Grid container spacing={2} mt={1} mb={6}>
|
||||
<Grid item xs={12}>
|
||||
@@ -105,9 +155,9 @@ const PageMixnodeDetailWithState = () => {
|
||||
)}
|
||||
{mixNodeRow?.blacklisted && (
|
||||
<Typography
|
||||
textAlign={isMobile ? 'left' : 'right'}
|
||||
textAlign={isMobile ? "left" : "right"}
|
||||
fontSize="smaller"
|
||||
sx={{ color: 'error.main' }}
|
||||
sx={{ color: "error.main" }}
|
||||
>
|
||||
This node is having a poor performance
|
||||
</Typography>
|
||||
@@ -153,7 +203,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since startup"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
keys={["Received", "Sent", "Explicitly dropped"]}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_startup || 0,
|
||||
stats?.data?.packets_sent_since_startup || 0,
|
||||
@@ -164,7 +214,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since last update"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
keys={["Received", "Sent", "Explicitly dropped"]}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_last_update || 0,
|
||||
stats?.data?.packets_sent_since_last_update || 0,
|
||||
@@ -204,7 +254,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
<TwoColSmallTable
|
||||
loading={status.isLoading}
|
||||
error={status?.error?.message}
|
||||
keys={['Mix port', 'Verloc port', 'HTTP port']}
|
||||
keys={["Mix port", "Verloc port", "HTTP port"]}
|
||||
values={[1789, 1790, 8000].map((each) => each)}
|
||||
icons={
|
||||
(status?.data?.ports && Object.values(status.data.ports)) || [
|
||||
@@ -237,29 +287,32 @@ const PageMixnodeDetailWithState = () => {
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard chat={true} overTitle="Test" />
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageMixnodeDetailGuard = () => {
|
||||
const { mixNode } = useMixnodeContext()
|
||||
const { id } = useParams()
|
||||
const { mixNode } = useMixnodeContext();
|
||||
const { id } = useParams();
|
||||
|
||||
if (mixNode?.isLoading) {
|
||||
return <CircularProgress />
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (mixNode?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(mixNode?.error)
|
||||
console.error(mixNode?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
Oh no! Could not load mixnode <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
@@ -267,31 +320,31 @@ const PageMixnodeDetailGuard = () => {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Mixnode not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return <PageMixnodeDetailWithState />
|
||||
}
|
||||
return <PageMixnodeDetailWithState />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
const PageMixnodeDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { id } = useParams();
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
if (!id || typeof id !== "string") {
|
||||
return (
|
||||
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MixnodeContextProvider mixId={id}>
|
||||
<PageMixnodeDetailGuard />
|
||||
</MixnodeContextProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PageMixnodeDetail
|
||||
export default PageMixnodeDetail;
|
||||
|
||||
@@ -1,26 +1,288 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { Box, Grid, Link, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import { PeopleAlt } from '@mui/icons-material'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { StatsCard } from '@/app/components/StatsCard'
|
||||
import { MixnodesSVG } from '@/app/icons/MixnodesSVG'
|
||||
import { Icons } from '@/app/components/Icons'
|
||||
import { GatewaysSVG } from '@/app/icons/GatewaysSVG'
|
||||
import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { WorldMap } from '@/app/components/WorldMap'
|
||||
import { BIG_DIPPER } from '@/app/api/constants'
|
||||
import { formatNumber } from '@/app/utils'
|
||||
import { useMainContext } from './context/main'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Grid, Link, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
import { PeopleAlt } from "@mui/icons-material";
|
||||
import { Title } from "@/app/components/Title";
|
||||
import { StatsCard } from "@/app/components/StatsCard";
|
||||
import { MixnodesSVG } from "@/app/icons/MixnodesSVG";
|
||||
import { Icons } from "@/app/components/Icons";
|
||||
import { GatewaysSVG } from "@/app/icons/GatewaysSVG";
|
||||
import { ValidatorsSVG } from "@/app/icons/ValidatorsSVG";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { WorldMap } from "@/app/components/WorldMap";
|
||||
import { BIG_DIPPER } from "@/app/api/constants";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import { useMainContext } from "./context/main";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ContentCardProps, ExplorerCard } from "./components/ExplorerCard";
|
||||
import { ExplorerData, getCacheExplorerData } from "./api/explorer";
|
||||
import { IExplorerLineChartData } from "./components/ExplorerLineChart";
|
||||
import {
|
||||
AccountStatsCard,
|
||||
IAccountStatsCardProps,
|
||||
} from "./components/AccountStatsCard";
|
||||
import TwoSidedSwitch from "./components/ExplorerSwitchButton";
|
||||
|
||||
const PageOverview = () => {
|
||||
const theme = useTheme()
|
||||
const router = useRouter()
|
||||
const explorerCard: ContentCardProps = {
|
||||
overTitle: "SINGLE",
|
||||
profileImage: {},
|
||||
title: "SINGLE",
|
||||
profileCountry: {
|
||||
countryCode: "NO",
|
||||
countryName: "Norway",
|
||||
},
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
rows: [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
{ key: "24H VOL", value: "$ 1000000" },
|
||||
],
|
||||
},
|
||||
graph: {
|
||||
data: [
|
||||
{
|
||||
date_utc: "2024-11-20",
|
||||
numericData: 10,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-21",
|
||||
numericData: 12,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-22",
|
||||
numericData: 9,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-23",
|
||||
numericData: 11,
|
||||
},
|
||||
],
|
||||
color: "#00CA33",
|
||||
label: "Label",
|
||||
},
|
||||
nymAddress: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
identityKey: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
qrCode: {
|
||||
url: "https://nymtech.net",
|
||||
},
|
||||
ratings: {
|
||||
ratings: [
|
||||
{ title: "Rating", numberOfStars: 4 },
|
||||
{ title: "Rating", numberOfStars: 2 },
|
||||
{ title: "Rating", numberOfStars: 3 },
|
||||
],
|
||||
},
|
||||
chat: true,
|
||||
paragraph: "Additional line",
|
||||
button: {
|
||||
onClick: () => {},
|
||||
label: "Label",
|
||||
},
|
||||
};
|
||||
export const DATA_REVALIDATE = 60;
|
||||
|
||||
export default function PageOverview() {
|
||||
const [explorerData, setExplorerData] = useState<ExplorerData | null>(null);
|
||||
const [noiseLineGraphData, setNoiseLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: IExplorerLineChartData[];
|
||||
}>();
|
||||
const [stakeLineGraphData, setStakeLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: IExplorerLineChartData[];
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const data = await getCacheExplorerData();
|
||||
setExplorerData(data);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
// CURRENT EPOCH
|
||||
const currentEpochStart =
|
||||
explorerData?.currentEpochData.current_epoch_start || "";
|
||||
|
||||
const progressBar = {
|
||||
title: "Current NGM epoch",
|
||||
start: currentEpochStart || "",
|
||||
showEpoch: true,
|
||||
};
|
||||
|
||||
const formatBigNum = (num: number) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B";
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
||||
}
|
||||
return num;
|
||||
}
|
||||
};
|
||||
|
||||
// NOISE
|
||||
|
||||
const noiseLast24H =
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_sent +
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_received;
|
||||
|
||||
const noisePrevious24H =
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_sent +
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_received;
|
||||
|
||||
const calculatePercentageChange = (last24H: number, previous24H: number) => {
|
||||
if (previous24H === 0) {
|
||||
throw new Error(
|
||||
"Cannot calculate percentage change when yesterday's value is zero."
|
||||
);
|
||||
}
|
||||
|
||||
const change = ((last24H - previous24H) / previous24H) * 100;
|
||||
|
||||
return parseFloat(change.toFixed(2));
|
||||
};
|
||||
|
||||
const percentage = calculatePercentageChange(noiseLast24H, noisePrevious24H);
|
||||
|
||||
const getPacketsData = () => {
|
||||
const data: Array<IExplorerLineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map((item: any) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
});
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const noiseLineGraphData = {
|
||||
color: "#8482FD",
|
||||
label: "Total packets sent and received",
|
||||
data: getPacketsData(),
|
||||
};
|
||||
setNoiseLineGraphData(noiseLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const noiseCard = {
|
||||
overTitle: "Noise generated last 24h",
|
||||
title: formatBigNum(noiseLast24H) || "",
|
||||
upDownLine: {
|
||||
percentage: Math.abs(percentage) || 0,
|
||||
numberWentUp: percentage > 0,
|
||||
},
|
||||
graph: noiseLineGraphData,
|
||||
};
|
||||
|
||||
// STAKE
|
||||
|
||||
const currentStake =
|
||||
Number(explorerData?.currentEpochRewardsData.interval.staking_supply) /
|
||||
1000000 || 0;
|
||||
|
||||
const getStakeData = () => {
|
||||
const data: Array<IExplorerLineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map((item: any) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_stake / 1000000,
|
||||
});
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const stakeLineGraphData = {
|
||||
color: "#00CA33",
|
||||
label: "Total stake delegated in NYM",
|
||||
data: getStakeData(),
|
||||
};
|
||||
setStakeLineGraphData(stakeLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const stakeCard = {
|
||||
overTitle: "Current network stake",
|
||||
title: currentStake + " NYM" || "",
|
||||
graph: stakeLineGraphData,
|
||||
};
|
||||
|
||||
const accountStatsCard: IAccountStatsCardProps = {
|
||||
overTitle: "Total value",
|
||||
priceTitle: 1990.0174,
|
||||
rows: [
|
||||
{ type: "Spendable", allocation: 15.53, amount: 12800, value: 1200 },
|
||||
{
|
||||
type: "Delegated",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Liquid", amount: 6900 },
|
||||
{ type: "Locked", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Claimable",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Unlocked", amount: 6900 },
|
||||
{ type: "Staking rewards", amount: 6900 },
|
||||
{ type: "Operator comission", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Self bonded",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
{
|
||||
type: "Locked",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {
|
||||
summaryOverview,
|
||||
@@ -29,9 +291,14 @@ const PageOverview = () => {
|
||||
block,
|
||||
countryData,
|
||||
serviceProviders,
|
||||
} = useMainContext()
|
||||
} = useMainContext();
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<TwoSidedSwitch
|
||||
leftLabel="Account"
|
||||
rightLabel="Mixnode"
|
||||
onSwitch={() => {}}
|
||||
/>
|
||||
<Grid>
|
||||
<Grid item paddingBottom={3}>
|
||||
<Title text="Overview" />
|
||||
@@ -40,19 +307,34 @@ const PageOverview = () => {
|
||||
<Grid container spacing={3}>
|
||||
{summaryOverview && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<AccountStatsCard {...accountStatsCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...explorerCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard progressBar={progressBar} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...noiseCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...stakeCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => router.push('/network-components/mixnodes')}
|
||||
onClick={() => router.push("/network-components/mixnodes")}
|
||||
title="Mixnodes"
|
||||
icon={<MixnodesSVG />}
|
||||
count={summaryOverview.data?.mixnodes.count || ''}
|
||||
count={summaryOverview.data?.mixnodes.count || ""}
|
||||
errorMsg={summaryOverview?.error}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/mixnodes?status=active')
|
||||
router.push("/network-components/mixnodes?status=active")
|
||||
}
|
||||
title="Active nodes"
|
||||
icon={<Icons.Mixnodes.Status.Active />}
|
||||
@@ -66,7 +348,7 @@ const PageOverview = () => {
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/mixnodes?status=standby')
|
||||
router.push("/network-components/mixnodes?status=standby")
|
||||
}
|
||||
title="Standby nodes"
|
||||
color={
|
||||
@@ -82,9 +364,9 @@ const PageOverview = () => {
|
||||
{gateways && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => router.push('/network-components/gateways')}
|
||||
onClick={() => router.push("/network-components/gateways")}
|
||||
title="Gateways"
|
||||
count={gateways?.data?.length || ''}
|
||||
count={gateways?.data?.length || ""}
|
||||
errorMsg={gateways?.error}
|
||||
icon={<GatewaysSVG />}
|
||||
/>
|
||||
@@ -94,7 +376,7 @@ const PageOverview = () => {
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/service-providers')
|
||||
router.push("/network-components/service-providers")
|
||||
}
|
||||
title="Service providers"
|
||||
icon={<PeopleAlt />}
|
||||
@@ -108,7 +390,7 @@ const PageOverview = () => {
|
||||
<StatsCard
|
||||
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
||||
title="Validators"
|
||||
count={validators?.data?.count || ''}
|
||||
count={validators?.data?.count || ""}
|
||||
errorMsg={validators?.error}
|
||||
icon={<ValidatorsSVG />}
|
||||
/>
|
||||
@@ -150,7 +432,5 @@ const PageOverview = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default PageOverview
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
declare module "react-world-flags";
|
||||
@@ -0,0 +1,46 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
remark:
|
||||
# remove the next line in case you want to use this Docker Compose file separately
|
||||
# as otherwise it would complain for absence of Dockerfile
|
||||
build: .
|
||||
image: umputun/remark42:latest
|
||||
container_name: "explorer_remark42"
|
||||
hostname: "remark42"
|
||||
restart: always
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
# uncomment to expose directly (no proxy)
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "443:8443"
|
||||
|
||||
environment:
|
||||
# - REMARK_URL=http://localhost:8081
|
||||
- REMARK_URL=https://remark.blockfend.com
|
||||
- SECRET=secret-key
|
||||
- AUTH_ANON=true
|
||||
# - SITE=remark42
|
||||
- SITE=nym-explorer-test
|
||||
|
||||
# - DEBUG=true
|
||||
# - AUTH_GOOGLE_CID
|
||||
# - AUTH_GOOGLE_CSEC
|
||||
# - AUTH_GITHUB_CID
|
||||
# - AUTH_GITHUB_CSEC
|
||||
# - AUTH_FACEBOOK_CID
|
||||
# - AUTH_FACEBOOK_CSEC
|
||||
# - AUTH_DISQUS_CID
|
||||
# - AUTH_DISQUS_CSEC
|
||||
# Enable it only for the initial comment import or for manual backups.
|
||||
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
|
||||
# to keep creating backups manually!
|
||||
- ADMIN_PASSWD=password
|
||||
volumes:
|
||||
- ./var:/srv/var
|
||||
@@ -9,15 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@mui/x-data-grid": "7.1.1",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@nymproject/nym-validator-client": "0.18.0",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"material-react-table": "^2.12.1",
|
||||
"next": "14.1.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"material-react-table": "^2.12.1",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-data-grid": "7.1.1"
|
||||
"react-world-flags": "^1.6.0",
|
||||
"qrcode.react": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -28,7 +28,8 @@
|
||||
"../assets/*"
|
||||
]
|
||||
},
|
||||
"moduleResolution": "node"
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
|
After Width: | Height: | Size: 976 B |
@@ -77,9 +77,11 @@ nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-network-requester = { path = "../service-providers/network-requester" }
|
||||
nym-node-http-api = { path = "../nym-node/nym-node-http-api" }
|
||||
nym-pemstore = { path = "../common/pemstore" }
|
||||
nym-sdk = { path = "../sdk/rust/nym-sdk" }
|
||||
nym-sphinx = { path = "../common/nymsphinx" }
|
||||
nym-statistics-common = { path = "../common/statistics" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-topology = { path = "../common/topology" }
|
||||
nym-types = { path = "../common/types" }
|
||||
nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::GatewayError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_gateway_stats_storage::PersistentStatsStorage;
|
||||
use nym_gateway_storage::PersistentStorage;
|
||||
use nym_pemstore::traits::PemStorableKeyPair;
|
||||
use nym_pemstore::KeyPairPath;
|
||||
|
||||
use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
|
||||
use nym_topology::{gateway, NymTopology, TopologyProvider};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
||||
pub async fn load_network_requester_config<P: AsRef<Path>>(
|
||||
id: &str,
|
||||
@@ -102,3 +107,56 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result<encryption::KeyPair, G
|
||||
);
|
||||
load_keypair(sphinx_paths, "gateway sphinx")
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GatewayTopologyProvider {
|
||||
inner: Arc<Mutex<GatewayTopologyProviderInner>>,
|
||||
}
|
||||
|
||||
impl GatewayTopologyProvider {
|
||||
pub fn new(
|
||||
gateway_node: gateway::LegacyNode,
|
||||
user_agent: UserAgent,
|
||||
nym_api_url: Vec<Url>,
|
||||
) -> GatewayTopologyProvider {
|
||||
GatewayTopologyProvider {
|
||||
inner: Arc::new(Mutex::new(GatewayTopologyProviderInner {
|
||||
inner: NymApiTopologyProvider::new(
|
||||
NymApiTopologyProviderConfig {
|
||||
min_mixnode_performance: 50,
|
||||
min_gateway_performance: 0,
|
||||
},
|
||||
nym_api_url,
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
Some(user_agent),
|
||||
),
|
||||
gateway_node,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GatewayTopologyProviderInner {
|
||||
inner: NymApiTopologyProvider,
|
||||
gateway_node: gateway::LegacyNode,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TopologyProvider for GatewayTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
let mut guard = self.inner.lock().await;
|
||||
match guard.inner.get_new_topology().await {
|
||||
None => None,
|
||||
Some(mut base) => {
|
||||
if !base.gateway_exists(&guard.gateway_node.identity_key) {
|
||||
debug!(
|
||||
"{} didn't exist in topology. inserting it.",
|
||||
guard.gateway_node.identity_key
|
||||
);
|
||||
base.insert_gateway(guard.gateway_node.clone());
|
||||
}
|
||||
Some(base)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle,
|
||||
use crate::node::client_handling::websocket;
|
||||
use crate::node::helpers::{
|
||||
initialise_main_storage, initialise_stats_storage, load_network_requester_config,
|
||||
GatewayTopologyProvider,
|
||||
};
|
||||
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_credential_verification::ecash::{
|
||||
credential_sender::CredentialHandlerConfig, EcashManager,
|
||||
};
|
||||
@@ -27,13 +29,15 @@ use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilte
|
||||
use nym_node_http_api::state::metrics::SharedSessionStats;
|
||||
use nym_statistics_common::events::{self, StatsEventSender};
|
||||
use nym_task::{TaskClient, TaskHandle, TaskManager};
|
||||
use nym_topology::NetworkAddress;
|
||||
use nym_types::gateway::GatewayNodeDetailsResponse;
|
||||
use nym_validator_client::client::NodeId;
|
||||
use nym_validator_client::nyxd::{Coin, CosmWasmClient};
|
||||
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use statistics::GatewayStatisticsCollector;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::*;
|
||||
@@ -236,6 +240,39 @@ impl<St> Gateway<St> {
|
||||
crate::helpers::node_details(&self.config).await
|
||||
}
|
||||
|
||||
fn gateway_topology_provider(&self) -> GatewayTopologyProvider {
|
||||
GatewayTopologyProvider::new(
|
||||
self.as_topology_node(),
|
||||
bin_info!().into(),
|
||||
self.config.gateway.nym_api_urls.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode {
|
||||
let ip = self
|
||||
.config
|
||||
.host
|
||||
.public_ips
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port);
|
||||
|
||||
nym_topology::gateway::LegacyNode {
|
||||
// those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate.
|
||||
// the only thing that matters is the identity key (and maybe version)
|
||||
node_id: NodeId::MAX,
|
||||
mix_host,
|
||||
host: NetworkAddress::IpAddr(ip),
|
||||
clients_ws_port: self.config.gateway.clients_port,
|
||||
clients_wss_port: self.config.gateway.clients_wss_port,
|
||||
sphinx_key: *self.sphinx_keypair.public_key(),
|
||||
|
||||
identity_key: *self.identity_keypair.public_key(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_mix_socket_listener(
|
||||
&self,
|
||||
ack_sender: MixForwardingSender,
|
||||
@@ -268,6 +305,7 @@ impl<St> Gateway<St> {
|
||||
async fn start_authenticator(
|
||||
&mut self,
|
||||
forwarding_channel: MixForwardingSender,
|
||||
topology_provider: GatewayTopologyProvider,
|
||||
shutdown: TaskClient,
|
||||
ecash_verifier: Arc<EcashManager<St>>,
|
||||
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>>
|
||||
@@ -315,6 +353,7 @@ impl<St> Gateway<St> {
|
||||
.with_shutdown(shutdown.fork("authenticator"))
|
||||
.with_wait_for_gateway(true)
|
||||
.with_minimum_gateway_performance(0)
|
||||
.with_custom_topology_provider(Box::new(topology_provider))
|
||||
.with_on_start(on_start_tx);
|
||||
|
||||
if let Some(custom_mixnet) = &opts.custom_mixnet_path {
|
||||
@@ -363,6 +402,7 @@ impl<St> Gateway<St> {
|
||||
async fn start_authenticator(
|
||||
&self,
|
||||
_forwarding_channel: MixForwardingSender,
|
||||
_topology_provider: GatewayTopologyProvider,
|
||||
_shutdown: TaskClient,
|
||||
_ecash_verifier: Arc<EcashManager<St>>,
|
||||
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> {
|
||||
@@ -435,6 +475,7 @@ impl<St> Gateway<St> {
|
||||
async fn start_network_requester(
|
||||
&self,
|
||||
forwarding_channel: MixForwardingSender,
|
||||
topology_provider: GatewayTopologyProvider,
|
||||
shutdown: TaskClient,
|
||||
) -> Result<StartedNetworkRequester, GatewayError> {
|
||||
info!("Starting network requester...");
|
||||
@@ -462,6 +503,7 @@ impl<St> Gateway<St> {
|
||||
.with_custom_gateway_transceiver(Box::new(transceiver))
|
||||
.with_wait_for_gateway(true)
|
||||
.with_minimum_gateway_performance(0)
|
||||
.with_custom_topology_provider(Box::new(topology_provider))
|
||||
.with_on_start(on_start_tx);
|
||||
|
||||
if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path {
|
||||
@@ -499,6 +541,7 @@ impl<St> Gateway<St> {
|
||||
async fn start_ip_packet_router(
|
||||
&self,
|
||||
forwarding_channel: MixForwardingSender,
|
||||
topology_provider: GatewayTopologyProvider,
|
||||
shutdown: TaskClient,
|
||||
) -> Result<LocalEmbeddedClientHandle, GatewayError> {
|
||||
info!("Starting IP packet provider...");
|
||||
@@ -527,6 +570,7 @@ impl<St> Gateway<St> {
|
||||
.with_custom_gateway_transceiver(Box::new(transceiver))
|
||||
.with_wait_for_gateway(true)
|
||||
.with_minimum_gateway_performance(0)
|
||||
.with_custom_topology_provider(Box::new(topology_provider))
|
||||
.with_on_start(on_start_tx);
|
||||
|
||||
if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path {
|
||||
@@ -643,6 +687,8 @@ impl<St> Gateway<St> {
|
||||
shutdown.fork("statistics::GatewayStatisticsCollector"),
|
||||
);
|
||||
|
||||
let topology_provider = self.gateway_topology_provider();
|
||||
|
||||
let handler_config = CredentialHandlerConfig {
|
||||
revocation_bandwidth_penalty: self
|
||||
.config
|
||||
@@ -691,6 +737,7 @@ impl<St> Gateway<St> {
|
||||
let embedded_nr = self
|
||||
.start_network_requester(
|
||||
mix_forwarding_channel.clone(),
|
||||
topology_provider.clone(),
|
||||
shutdown.fork("NetworkRequester"),
|
||||
)
|
||||
.await?;
|
||||
@@ -706,6 +753,7 @@ impl<St> Gateway<St> {
|
||||
let embedded_ip_sp = self
|
||||
.start_ip_packet_router(
|
||||
mix_forwarding_channel.clone(),
|
||||
topology_provider.clone(),
|
||||
shutdown.fork("ip_service_provider"),
|
||||
)
|
||||
.await?;
|
||||
@@ -718,6 +766,7 @@ impl<St> Gateway<St> {
|
||||
let embedded_auth = self
|
||||
.start_authenticator(
|
||||
mix_forwarding_channel,
|
||||
topology_provider,
|
||||
shutdown.fork("authenticator"),
|
||||
ecash_verifier,
|
||||
)
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges;
|
||||
use nym_mixnet_contract_common::{
|
||||
GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding,
|
||||
};
|
||||
use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges;
|
||||
use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
@@ -64,7 +62,7 @@ pub struct LegacyMixNodeDetailsWithLayer {
|
||||
|
||||
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
|
||||
#[serde(default)]
|
||||
pub pending_changes: PendingMixNodeChanges,
|
||||
pub pending_changes: LegacyPendingMixNodeChanges,
|
||||
}
|
||||
|
||||
impl LegacyMixNodeDetailsWithLayer {
|
||||
@@ -80,13 +78,3 @@ impl LegacyMixNodeDetailsWithLayer {
|
||||
self.bond_information.is_unbonding
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LegacyMixNodeDetailsWithLayer> for MixNodeDetails {
|
||||
fn from(value: LegacyMixNodeDetailsWithLayer) -> Self {
|
||||
MixNodeDetails {
|
||||
bond_information: value.bond_information.into(),
|
||||
rewarding_details: value.rewarding_details,
|
||||
pending_changes: value.pending_changes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
use nym_mixnet_contract_common::rewarding::RewardEstimate;
|
||||
use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent};
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent};
|
||||
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
|
||||
use nym_node_requests::api::v1::authenticator::models::Authenticator;
|
||||
use nym_node_requests::api::v1::gateway::models::Wireguard;
|
||||
@@ -138,6 +138,48 @@ pub struct NodePerformance {
|
||||
pub last_24h: Performance,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
|
||||
)]
|
||||
pub enum DisplayRole {
|
||||
EntryGateway,
|
||||
Layer1,
|
||||
Layer2,
|
||||
Layer3,
|
||||
ExitGateway,
|
||||
Standby,
|
||||
}
|
||||
|
||||
impl From<Role> for DisplayRole {
|
||||
fn from(role: Role) -> Self {
|
||||
match role {
|
||||
Role::EntryGateway => DisplayRole::EntryGateway,
|
||||
Role::Layer1 => DisplayRole::Layer1,
|
||||
Role::Layer2 => DisplayRole::Layer2,
|
||||
Role::Layer3 => DisplayRole::Layer3,
|
||||
Role::ExitGateway => DisplayRole::ExitGateway,
|
||||
Role::Standby => DisplayRole::Standby,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DisplayRole> for Role {
|
||||
fn from(role: DisplayRole) -> Self {
|
||||
match role {
|
||||
DisplayRole::EntryGateway => Role::EntryGateway,
|
||||
DisplayRole::Layer1 => Role::Layer1,
|
||||
DisplayRole::Layer2 => Role::Layer2,
|
||||
DisplayRole::Layer3 => Role::Layer3,
|
||||
DisplayRole::ExitGateway => Role::ExitGateway,
|
||||
DisplayRole::Standby => Role::Standby,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// imo for now there's no point in exposing more than that,
|
||||
// nym-api shouldn't be calculating apy or stake saturation for you.
|
||||
// it should just return its own metrics (performance) and then you can do with it as you wish
|
||||
@@ -153,7 +195,7 @@ pub struct NodePerformance {
|
||||
pub struct NodeAnnotation {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
pub last_24h_performance: Performance,
|
||||
pub current_role: Option<Role>,
|
||||
pub current_role: Option<DisplayRole>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
@@ -286,7 +328,7 @@ impl MixNodeBondAnnotated {
|
||||
.sphinx_key
|
||||
.parse()
|
||||
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
|
||||
epoch_role: role,
|
||||
role,
|
||||
supported_roles: DeclaredRoles {
|
||||
mixnode: true,
|
||||
entry: false,
|
||||
@@ -345,7 +387,7 @@ impl GatewayBondAnnotated {
|
||||
.sphinx_key
|
||||
.parse()
|
||||
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
|
||||
epoch_role: role,
|
||||
role,
|
||||
supported_roles: DeclaredRoles {
|
||||
mixnode: false,
|
||||
entry: true,
|
||||
@@ -810,6 +852,10 @@ impl NymNodeDescription {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
|
||||
self.description.host_information.keys.ed25519
|
||||
}
|
||||
|
||||
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
|
||||
let keys = &self.description.host_information.keys;
|
||||
let entry = if self.description.declared_role.entry {
|
||||
@@ -827,7 +873,7 @@ impl NymNodeDescription {
|
||||
// we can't use the declared roles, we have to take whatever was provided in the contract.
|
||||
// why? say this node COULD operate as an exit, but it might be the case the contract decided
|
||||
// to assign it an ENTRY role only. we have to use that one instead.
|
||||
epoch_role: role,
|
||||
role,
|
||||
supported_roles: self.description.declared_role,
|
||||
entry,
|
||||
performance,
|
||||
@@ -935,14 +981,14 @@ impl NymNodeData {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct LegacyDescribedGateway {
|
||||
pub bond: LegacyGatewayBondWithId,
|
||||
pub bond: GatewayBond,
|
||||
pub self_described: Option<NymNodeData>,
|
||||
}
|
||||
|
||||
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
|
||||
fn from(bond: LegacyGatewayBondWithId) -> Self {
|
||||
LegacyDescribedGateway {
|
||||
bond,
|
||||
bond: bond.bond,
|
||||
self_described: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ pub struct SkimmedNode {
|
||||
#[schemars(with = "String")]
|
||||
pub x25519_sphinx_pubkey: x25519::PublicKey,
|
||||
|
||||
#[serde(alias = "role")]
|
||||
pub epoch_role: NodeRole,
|
||||
#[serde(alias = "epoch_role")]
|
||||
pub role: NodeRole,
|
||||
|
||||
// needed for the purposes of sending appropriate test packets
|
||||
#[serde(default)]
|
||||
@@ -157,7 +157,7 @@ pub struct SkimmedNode {
|
||||
|
||||
impl SkimmedNode {
|
||||
pub fn get_mix_layer(&self) -> Option<u8> {
|
||||
match self.epoch_role {
|
||||
match self.role {
|
||||
NodeRole::Mixnode { layer } => Some(layer),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -24,21 +24,21 @@ use utoipa::IntoParams;
|
||||
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/master-verification-key:epoch_id",
|
||||
"/master-verification-key",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| master_verification_key(epoch_id, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-expiration-date-signatures:expiration_date",
|
||||
"/aggregated-expiration-date-signatures",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/aggregated-coin-indices-signatures:epoch_id",
|
||||
"/aggregated-coin-indices-signatures",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
|
||||
@@ -52,7 +52,7 @@ pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppStat
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/master-verification-key/{epoch_id}",
|
||||
path = "/v1/ecash/master-verification-key",
|
||||
responses(
|
||||
(status = 200, body = VerificationKeyResponse)
|
||||
)
|
||||
@@ -83,7 +83,7 @@ struct ExpirationDateParam {
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
|
||||
path = "/v1/ecash/aggregated-expiration-date-signatures",
|
||||
responses(
|
||||
(status = 200, body = AggregatedExpirationDateSignatureResponse)
|
||||
)
|
||||
@@ -120,7 +120,7 @@ async fn expiration_date_signatures(
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
|
||||
path = "/v1/ecash/aggregated-coin-indices-signatures",
|
||||
responses(
|
||||
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
|
||||
)
|
||||
|
||||
@@ -32,14 +32,14 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-expiration-date-signatures:expiration_date",
|
||||
"/partial-expiration-date-signatures",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/partial-coin-indices-signatures:epoch_id",
|
||||
"/partial-coin-indices-signatures",
|
||||
axum::routing::get({
|
||||
let ecash_state = Arc::clone(&ecash_state);
|
||||
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
|
||||
@@ -127,7 +127,7 @@ struct ExpirationDateParam {
|
||||
params(
|
||||
ExpirationDateParam
|
||||
),
|
||||
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
|
||||
path = "/v1/ecash/partial-expiration-date-signatures",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse),
|
||||
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
|
||||
@@ -165,7 +165,7 @@ async fn partial_expiration_date_signatures(
|
||||
params(
|
||||
EpochIdParam
|
||||
),
|
||||
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
|
||||
path = "/v1/ecash/partial-coin-indices-signatures",
|
||||
responses(
|
||||
(status = 200, body = PartialExpirationDateSignatureResponse),
|
||||
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
|
||||
|
||||
@@ -20,6 +20,7 @@ use time::macros::time;
|
||||
use time::{OffsetDateTime, Time};
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
@@ -242,6 +243,7 @@ async fn batch_redeem_tickets(
|
||||
(status = 500, body = ErrorResponse, description = "bloomfilters got disabled"),
|
||||
)
|
||||
)]
|
||||
#[deprecated]
|
||||
async fn double_spending_filter_v1(
|
||||
_state: Arc<EcashState>,
|
||||
) -> AxumResult<Json<SpentCredentialsResponse>> {
|
||||
|
||||
@@ -169,7 +169,7 @@ impl EpochAdvancer {
|
||||
let standby_eligible = all_choices
|
||||
.iter()
|
||||
.filter(|node| {
|
||||
exit_gateways.contains(&node.0.node_id)
|
||||
!exit_gateways.contains(&node.0.node_id)
|
||||
&& !entry_gateways.contains(&node.0.node_id)
|
||||
&& !mixnodes.contains(&node.0.node_id)
|
||||
})
|
||||
@@ -228,14 +228,24 @@ impl EpochAdvancer {
|
||||
)
|
||||
}
|
||||
|
||||
Ok(RewardedSet {
|
||||
let mut rewarded_set = RewardedSet {
|
||||
entry_gateways: entry_gateways.into_iter().collect(),
|
||||
exit_gateways: exit_gateways.into_iter().collect(),
|
||||
layer1,
|
||||
layer2,
|
||||
layer3,
|
||||
standby,
|
||||
})
|
||||
};
|
||||
|
||||
// make sure to sort the rewarded set values
|
||||
rewarded_set.entry_gateways.sort();
|
||||
rewarded_set.exit_gateways.sort();
|
||||
rewarded_set.layer1.sort();
|
||||
rewarded_set.layer2.sort();
|
||||
rewarded_set.layer3.sort();
|
||||
rewarded_set.standby.sort();
|
||||
|
||||
Ok(rewarded_set)
|
||||
}
|
||||
|
||||
async fn attach_performance_to_eligible_nodes(
|
||||
|
||||
@@ -274,8 +274,8 @@ impl<R: MessageReceiver + Send> Monitor<R> {
|
||||
info!("Received {}/{} packets", total_received, total_sent);
|
||||
|
||||
let summary = self.summary_producer.produce_summary(
|
||||
prepared_packets.tested_mixnodes,
|
||||
prepared_packets.tested_gateways,
|
||||
prepared_packets.mixnodes_under_test,
|
||||
prepared_packets.gateways_under_test,
|
||||
received,
|
||||
prepared_packets.invalid_mixnodes,
|
||||
prepared_packets.invalid_gateways,
|
||||
|
||||