Compare commits

...

61 Commits

Author SHA1 Message Date
Yana 7eddb12cbe wip 2024-12-06 15:22:52 +07:00
Yana e3fdc9aa12 wip 2024-12-05 21:21:59 +07:00
Yana 4e7ece13c5 wip 2024-12-05 17:39:20 +07:00
Yana 38f2a052d2 Add styling to account stats card 2024-12-05 16:26:30 +07:00
Yana 565e9c5a40 Add account stats card, mobile view 2024-12-04 22:16:53 +07:00
Yana b6465836d8 Add account stats card, desktop view 2024-12-04 19:56:08 +07:00
Yana 40916f77da wip 2024-12-03 16:11:38 +07:00
Yana 492fe16e55 wip 2024-12-02 22:18:53 +07:00
Yana cf3baf9398 Add chat to the card 2024-12-01 21:05:24 +07:00
Yana c3f462c34d add styles 2024-11-29 21:45:36 +07:00
Yana b95a026ddd add ratings 2024-11-29 21:17:24 +07:00
Yana 48a38b6bf3 Add qr code 2024-11-29 20:39:24 +07:00
Yana 57e6fa29db Add copy to clipboard 2024-11-29 20:06:09 +07:00
Yana 1856ac95c6 Add profile image and country 2024-11-29 19:12:13 +07:00
Yana bc6d4562d0 WIP 2024-11-28 22:19:31 +07:00
Yana 4887cbbd48 WIP 2024-11-27 22:34:49 +07:00
Yana 3c44ae89da WIP 2024-11-27 15:41:20 +07:00
Yana a9c9381cb6 WIP 2024-11-26 21:44:37 +07:00
Yana fbd8cc5b4d Add ExplorerCard 2024-11-24 21:25:23 +07:00
Yana 399e4b1abd WIP Explorer Card 2024-11-20 20:01:36 +07:00
Yana fd62ee8204 Add remark42 to Mixnode page 2024-11-04 20:25:41 +07:00
Yana 147ec12a28 WIP on yana/remark42 2024-10-31 17:46:04 +02:00
Dinko Zdravac c740f84336 NS API with directory v2 (#5058)
* Use unstable explorer client

* Clean up stale testruns & logging
- log gw identity key
- better agent testrun logging
- log responses
- change response code for agents

* Better logging on agent

* Testrun stores gw identity key instead of gw pk

* Agent 0.1.3

* Agent 0.1.4

* Sqlx offline query data + clippy

* Compatible with directory v2

* Point to internal deps + rebase + v0.1.5

* self described field not null

* Fix build.rs typo
2024-10-31 04:32:41 +01:00
Jędrzej Stuczyński 16de47ba57 Merge pull request #5063 from nymtech/merge2/release/2024.13-magura
Merge2/release/2024.13 magura
2024-10-30 14:30:11 +00:00
Jędrzej Stuczyński 54a823311b Merge branch 'release/2024.13-magura' into develop 2024-10-30 14:16:07 +00:00
Jędrzej Stuczyński 753a21f8ca bugfix/feature: added NymApiClient method to get all skimmed nodes (#5062)
* bugfix/feature: added NymApiClient method to get all skimmed nodes

* wasm

* helper: utility method for getting ed25519 identity directly from node description
2024-10-30 12:21:27 +00:00
Jędrzej Stuczyński 76da4ab532 bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set (#5049) 2024-10-30 09:11:13 +00:00
dependabot[bot] 2ca7c7a252 build(deps): bump lazy_static from 1.4.0 to 1.5.0 (#4913) 2024-10-30 07:07:39 +01:00
dependabot[bot] e680e8dc49 build(deps): bump once_cell from 1.19.0 to 1.20.2 (#4952)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.19.0 to 1.20.2.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.19.0...v1.20.2)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 23:27:10 +01:00
Jon Häggblad 242bc93807 Merge pull request #5027 from nymtech/jon/integrate-credential-proxy-into-workspace
Integrate nym-credential-proxy into workspace
2024-10-29 20:47:07 +01:00
dynco-nym 94c6cdc7b2 Type coercion into time::Date 2024-10-29 17:46:35 +01:00
Jon Häggblad fce322c789 Remove unused workflow 2024-10-29 17:46:35 +01:00
Jon Häggblad ac5baab693 Add to default workspace 2024-10-29 17:46:35 +01:00
Jon Häggblad 23da0f4d8e Workspace updates 2024-10-29 17:46:35 +01:00
Jon Häggblad 25e3b4cd83 Delete old Cargo files 2024-10-29 17:46:35 +01:00
Jon Häggblad 8e4d72a565 Update for rebase 2024-10-29 17:46:34 +01:00
Jon Häggblad ad84a6d85d Add nym-vpn-api crates to main workspace 2024-10-29 17:45:56 +01:00
Jędrzej Stuczyński 34c5f23684 Merge pull request #5061 from nymtech/merge1/release/2024.13-magura
checkpoint merge release/2024.13-magura into develop
2024-10-29 16:17:16 +00:00
Jędrzej Stuczyński 000f2f1c29 Merge branch 'release/2024.13-magura' into develop 2024-10-29 15:31:51 +00:00
Jędrzej Stuczyński 317f7fffa9 added hacky routes to return nymnodes alongside legacy nodes (#5051)
* added hacky routes to return nymnodes alongside legacy nodes

* fixed mixing role

* Update client (#5054)

* removed hacky mixnodes endpoint for its not used

* construct explorer-api client with timeout

---------

Co-authored-by: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com>
2024-10-29 08:35:07 +00:00
Jędrzej Stuczyński 4396def133 bugfix: adjust runtime storage migration (#5047) 2024-10-28 10:07:51 +00:00
Jędrzej Stuczyński a56a318a7f bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using query parameters (#5046) 2024-10-28 09:57:14 +00:00
Jędrzej Stuczyński 4d08047c57 bugfix: restore default http port for nym-api (#5045)
when it was run under 'rocket' server the port used was 8000. let's restore that value
2024-10-28 09:28:47 +00:00
Jędrzej Stuczyński cb13be27f8 bugfix: fix ecash handlers routes (#5043) 2024-10-28 09:12:40 +00:00
Jędrzej Stuczyński fa392169c1 bugfix: use human readable roles for annotations (#5036)
* bugfix: use human readable roles for annotations

* update the wallet code to use 'DisplayRole'
2024-10-28 09:08:17 +00:00
Jędrzej Stuczyński 3167fb34e6 bugfix: don't assign exit gateways to standby set (#5041) 2024-10-25 16:53:51 +01:00
Jędrzej Stuczyński 9ca6301e1c bugfix: make sure nym-nodes are also tested by network monitor (#5040) 2024-10-25 15:20:39 +01:00
Jędrzej Stuczyński e16a73338e bugfix: use bonded nym-nodes for determining initial network monitor nodes (#5039) 2024-10-25 12:34:25 +01:00
Bogdan-Ștefan Neacşu bfa3825d70 Pass the Poisson flag on authenticator config (#5037) 2024-10-25 14:08:52 +03:00
Jędrzej Stuczyński d626e7689f bugfix: make gateways insert themselves into [local] topology (#5038)
* added explicit SP suffix to started tasks

* added 'GatewayTopologyProvider' that always injects itself into the network

* use the new topology provider to bypass described bootstrapping problem
2024-10-25 12:06:16 +01:00
Jędrzej Stuczyński 9234474565 bugfix: use old name for 'epoch_role' in SkimmedNode (#5034)
* bugfix: use old name for 'epoch_role' in SkimmedNode

* clippy
2024-10-25 09:29:37 +01:00
Jędrzej Stuczyński 29f8386b50 bugfix: make sure to use correct highest node id when assigning role (#5032)
* bugfix: make sure to use correct highest node id when assigning role

* make sure nym-api provides sorted values for older contracts
2024-10-24 17:47:57 +01:00
Jędrzej Stuczyński 0edb9631a6 feature: use axum_client_ip for attempting to extract source ip (#5031) 2024-10-24 17:38:32 +01:00
Jędrzej Stuczyński 4b0153f5f2 bugfix: fixed backwards incompatibility for /gateways/described endpoint (#5030) 2024-10-24 15:37:41 +01:00
Jędrzej Stuczyński c09a17b66d bugfix: verifying signed information of legacy nodes (#5029)
* Added new legacy variant of HostInformation

* fixed 'option_bs58_x25519_pubkey' for empty string

* 'Debug' impl for x25519 and ed25519 to use human-readable representation

* HttpClient to use explicit 'serde_json' conversion for better errors

* additional 'Debug' derives
2024-10-24 15:00:34 +01:00
Jędrzej Stuczyński d18ddcdc11 bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' (#5028)
* bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change'

* updated schema files due to removal of '#[serde(deny_unknown_fields)]'
2024-10-24 10:54:00 +01:00
Jędrzej Stuczyński d2df542280 bugfix: missing #[serde(default)] for announce port (#5024) 2024-10-23 16:52:17 +01:00
Jędrzej Stuczyński 6fafd8c03a bugfix: directory v2.1 get_all_avg_gateway_reliability_in_interval query (#5023)
* log full storage errors on failures

* use query_as! macro
2024-10-23 16:36:21 +01:00
Jędrzej Stuczyński 38e66f6ddf added 'get_all_described_nodes' to NymApiClient and adjusted return type on api itself (#5016) 2024-10-23 09:48:25 +01:00
Bogdan-Ștefan Neacşu b9fbe0b8f3 Reapply fixes to new branch (#5014) 2024-10-22 18:33:18 +03:00
Bogdan-Ștefan Neacşu daafb5cae4 Consume only positive bandwidth (#5013) 2024-10-22 17:46:46 +03:00
182 changed files with 9288 additions and 10839 deletions
@@ -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
Generated
+450 -719
View File
File diff suppressed because it is too large Load Diff
+22 -21
View File
@@ -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) => {
+3 -1
View File
@@ -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"], &params)
.await
}
async fn get_nym_nodes(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], &params)
.await
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(
&self,
@@ -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"],
&params,
)
.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()
}
}
}
}
}
+9 -3
View File
@@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, 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)
}
}
+13 -12
View File
@@ -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 {
+1
View File
@@ -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
+3 -3
View File
@@ -1,18 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::{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 {
+1 -2
View File
@@ -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,
}
+4
View File
@@ -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
}
+1 -1
View File
@@ -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)?,
+1 -1
View File
@@ -68,7 +68,7 @@ pub async fn current_network_topology_async(
let api_client = NymApiClient::new(url);
let mixnodes = api_client
.get_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?;
+5 -2
View File
@@ -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,
+7 -6
View File
@@ -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);
+15 -15
View File
@@ -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());
+6 -3
View File
@@ -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
+32 -2
View File
@@ -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)
}
}
+9
View File
@@ -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;
+2
View File
@@ -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
+1
View File
@@ -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;
+26
View File
@@ -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)
}
+8
View File
@@ -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>;
+10
View File
@@ -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);
+154
View File
@@ -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
}
}
+9
View File
@@ -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");
+54 -5
View File
@@ -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");
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+40
View File
@@ -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
+50
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+24
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
@@ -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>
);
}
+27
View File
@@ -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"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.
Binary file not shown.
+190
View File
@@ -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();
// }
+18
View File
@@ -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>
);
};
+37
View File
@@ -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>
);
};
+7 -7
View File
@@ -1,15 +1,15 @@
import type { Metadata } from 'next'
import '@interchain-ui/react/styles'
import { App } from './App'
import type { Metadata } from "next";
import "@interchain-ui/react/styles";
import { App } from "./App";
export const metadata: Metadata = {
title: 'Nym Network Explorer',
}
title: "Nym Network Explorer",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) {
return (
<html lang="en">
@@ -17,5 +17,5 @@ export default function RootLayout({
<App>{children}</App>
</body>
</html>
)
);
}
@@ -1,72 +1,72 @@
'use client'
"use client";
import * as React from 'react'
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
import { useParams } from 'next/navigation'
import { GatewayBond } from '@/app/typeDefs/explorer-api'
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
import * as React from "react";
import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material";
import { useParams } from "next/navigation";
import { GatewayBond } from "@/app/typeDefs/explorer-api";
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
import {
gatewayEnrichedToGridRow,
GatewayEnrichedRowType,
} from '@/app/components/Gateways/Gateways'
import { ComponentError } from '@/app/components/ComponentError'
import { ContentCard } from '@/app/components/ContentCard'
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
import { UptimeChart } from '@/app/components/UptimeChart'
} from "@/app/components/Gateways/Gateways";
import { ComponentError } from "@/app/components/ComponentError";
import { ContentCard } from "@/app/components/ContentCard";
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
import { UptimeChart } from "@/app/components/UptimeChart";
import {
GatewayContextProvider,
useGatewayContext,
} from '@/app/context/gateway'
import { useMainContext } from '@/app/context/main'
import { Title } from '@/app/components/Title'
} from "@/app/context/gateway";
import { useMainContext } from "@/app/context/main";
import { Title } from "@/app/components/Title";
const columns: ColumnsType[] = [
{
field: 'identity_key',
title: 'Identity Key',
headerAlign: 'left',
field: "identity_key",
title: "Identity Key",
headerAlign: "left",
width: 230,
},
{
field: 'bond',
title: 'Bond',
headerAlign: 'left',
field: "bond",
title: "Bond",
headerAlign: "left",
},
{
field: 'node_performance',
title: 'Routing Score',
headerAlign: 'left',
field: "node_performance",
title: "Routing Score",
headerAlign: "left",
tooltipInfo:
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
},
{
field: 'avgUptime',
title: 'Avg. Score',
headerAlign: 'left',
field: "avgUptime",
title: "Avg. Score",
headerAlign: "left",
tooltipInfo: "Gateway's average routing score in the last 24 hours",
},
{
field: 'host',
title: 'IP',
headerAlign: 'left',
field: "host",
title: "IP",
headerAlign: "left",
width: 99,
},
{
field: 'location',
title: 'Location',
headerAlign: 'left',
field: "location",
title: "Location",
headerAlign: "left",
},
{
field: 'owner',
title: 'Owner',
headerAlign: 'left',
field: "owner",
title: "Owner",
headerAlign: "left",
},
{
field: 'version',
title: 'Version',
headerAlign: 'left',
field: "version",
title: "Version",
headerAlign: "left",
},
]
];
/**
* Shows gateway details
@@ -74,26 +74,26 @@ const columns: ColumnsType[] = [
const PageGatewayDetailsWithState = ({
selectedGateway,
}: {
selectedGateway: GatewayBond | undefined
selectedGateway: GatewayBond | undefined;
}) => {
const [enrichGateway, setEnrichGateway] =
React.useState<GatewayEnrichedRowType>()
const [status, setStatus] = React.useState<number[] | undefined>()
const { uptimeReport, uptimeStory } = useGatewayContext()
React.useState<GatewayEnrichedRowType>();
const [status, setStatus] = React.useState<number[] | undefined>();
const { uptimeReport, uptimeStory } = useGatewayContext();
React.useEffect(() => {
if (uptimeReport?.data && selectedGateway) {
setEnrichGateway(
gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)
)
);
}
}, [uptimeReport, selectedGateway])
}, [uptimeReport, selectedGateway]);
React.useEffect(() => {
if (enrichGateway) {
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort])
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
}
}, [enrichGateway])
}, [enrichGateway]);
return (
<Box component="main">
@@ -115,7 +115,7 @@ const PageGatewayDetailsWithState = ({
<ContentCard title="Gateway Status">
<TwoColSmallTable
loading={false}
keys={['Mix port', 'Client WS API Port']}
keys={["Mix port", "Client WS API Port"]}
values={status.map((each) => each)}
icons={status.map((elem) => !!elem)}
/>
@@ -137,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;
+313 -33
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
declare module "react-world-flags";
+46
View File
@@ -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
+7 -4
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

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

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.
Binary file not shown.
+2
View File
@@ -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" }
+60 -2
View File
@@ -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)
}
}
}
}
+50 -1
View File
@@ -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,
)
+3 -15
View File
@@ -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,
}
}
}
+53 -7
View File
@@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{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,
}
}
+3 -3
View File
@@ -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,
}
+6 -6
View File
@@ -24,21 +24,21 @@ use utoipa::IntoParams;
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
"/master-verification-key: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"),
+2
View File
@@ -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(
+2 -2
View File
@@ -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,

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