Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f87b90d55d | |||
| 833a75421b | |||
| db0922bf7c | |||
| cdb6f39341 | |||
| 20188ff373 | |||
| bd576f05c0 | |||
| 005d9e9d19 | |||
| 1a0748ae1f | |||
| e5dfd9ad02 | |||
| 4fed2a8f8c | |||
| 05108693ee | |||
| 990745e749 | |||
| 4dedf0bc75 | |||
| 3b53f380a2 | |||
| 5301edaf62 | |||
| 5990d4feeb | |||
| 45aaaf5d7f | |||
| 3691f17bf7 | |||
| 621d864ed4 | |||
| 4b71331ada | |||
| a136c8b955 | |||
| 9f7d7d98b8 | |||
| 7863df17e4 | |||
| 55a0f80d73 | |||
| af3271cb07 | |||
| e4d41e0c18 | |||
| a26bda03e9 | |||
| 248241f438 | |||
| 1a8dcc8a25 | |||
| 355a5f9e55 | |||
| 412e2f23e5 | |||
| 961f6c53b2 | |||
| 4bb67da497 | |||
| c4410bd187 | |||
| 7d1250b193 | |||
| 07065e025f | |||
| 812393be1a | |||
| 2903481edb | |||
| 7ee6b1bd42 | |||
| e5f6a82092 | |||
| 836720cd31 | |||
| 7b9d22d7e3 | |||
| a87f8deda1 | |||
| 56cf93f1b5 | |||
| 5dd63157cc | |||
| 9f920e1e9c | |||
| 64823b06c7 | |||
| fb689a35e6 | |||
| 1847a84372 | |||
| 65d1589968 | |||
| b3d87156db | |||
| 28b4fe7e7e | |||
| 9479d2a383 | |||
| 886b4410aa | |||
| b51358fb12 | |||
| 53e3acaa37 | |||
| 978817baf7 | |||
| 9319a5ec04 | |||
| 3186db2915 | |||
| ff7671f28a | |||
| cbe8eec2a4 | |||
| 42f9edd408 | |||
| 128cf7c070 | |||
| 79e5004849 | |||
| 0d6722f9f5 | |||
| d458df9c34 | |||
| 7a8ac59a36 | |||
| ad3eb7a84c | |||
| 135f248eba | |||
| 7012bf9886 | |||
| 88aa32ddeb | |||
| 7c1c9976f0 | |||
| 4ee7f7eaf5 | |||
| 778772d96a | |||
| 5b791b41aa | |||
| 4b7e51fc3b | |||
| 0a42dd3e0d | |||
| 7cf49f642d | |||
| 089ab65dd7 | |||
| c1fabae770 | |||
| 3ed7cfa381 | |||
| 4fe83da99d | |||
| 4f81fc7400 | |||
| 6d601ca654 | |||
| cea3ad9908 | |||
| e4ecd099cc | |||
| 0723542c39 | |||
| 523e559ff8 | |||
| 02b27573de | |||
| 8f229737a3 | |||
| 1afd13d6e0 | |||
| df10b5595a | |||
| 443031ba66 | |||
| 8d340a49d3 | |||
| e0925d3c7f | |||
| 89d391da29 | |||
| cc2d7d34d2 | |||
| 969070f938 | |||
| 3dfcae9369 | |||
| 32a4bf1172 | |||
| 433cac8c58 | |||
| 4fc64a072c |
+58
@@ -0,0 +1,58 @@
|
||||
# Security and sensitive files
|
||||
.env*
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
secrets/
|
||||
private/
|
||||
config/secrets/
|
||||
|
||||
# Development files
|
||||
node_modules/
|
||||
.npm/
|
||||
.npmrc
|
||||
.nvmrc
|
||||
*.log
|
||||
*.tmp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
target/
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Test files
|
||||
test/
|
||||
tests/
|
||||
__tests__/
|
||||
*.test.js
|
||||
*.test.ts
|
||||
*.spec.js
|
||||
*.spec.ts
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# CI/CD files
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
.circleci/
|
||||
azure-pipelines.yml
|
||||
|
||||
# Scripts
|
||||
scripts/
|
||||
!scripts/security-check.sh
|
||||
@@ -0,0 +1,21 @@
|
||||
audit-level=moderate
|
||||
fund=false
|
||||
update-notifier=false
|
||||
ignore-scripts=false
|
||||
strict-ssl=true
|
||||
|
||||
registry=https://registry.npmjs.org/
|
||||
audit=true
|
||||
package-lock=true
|
||||
package-lock-only=false
|
||||
save-exact=false
|
||||
|
||||
# use npm ci for production builds (faster and more secure)
|
||||
# this will be enforced in CI/CD scripts
|
||||
|
||||
# prevent installation of optional dependencies that might contain vulnerabilities
|
||||
optional=false
|
||||
audit=true
|
||||
update-notifier=false
|
||||
|
||||
save-exact=false
|
||||
@@ -0,0 +1 @@
|
||||
20
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["node_modules", ".next", "public"]
|
||||
"ignore": ["node_modules", ".next", "public", "src/app/lib/strapi.d.ts"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
||||
+16
-16
@@ -2,23 +2,23 @@
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: true,
|
||||
|
||||
basePath: "/explorer",
|
||||
assetPrefix: "/explorer",
|
||||
trailingSlash: false,
|
||||
basePath: "/explorer",
|
||||
assetPrefix: "/explorer",
|
||||
trailingSlash: false,
|
||||
|
||||
async redirects() {
|
||||
return [
|
||||
// Change the basePath to /explorer
|
||||
{
|
||||
source: "/",
|
||||
destination: "/explorer",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
// Change the basePath to /explorer
|
||||
{
|
||||
source: "/",
|
||||
destination: "/explorer",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"prebuild": "yarn --cwd ../ts-packages/types build && yarn --cwd ../sdk/typescript/packages/mui-theme build && yarn --cwd ../sdk/typescript/packages/react-components build",
|
||||
"build:prod": "yarn --cwd .. build && next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check --fix"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": "20.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chain-registry/types": "^0.50.36",
|
||||
"@cosmos-kit/keplr-extension": "^2.14.0",
|
||||
"@cosmos-kit/react": "^2.20.1",
|
||||
"@cosmos-kit/keplr-extension": "^2.17.0",
|
||||
"@cosmos-kit/react": "^2.24.0",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
@@ -35,6 +36,7 @@
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"chain-registry": "^1.69.64",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
@@ -49,6 +51,8 @@
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
@@ -57,9 +61,10 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"lefthook": "^1.8.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
@@ -7,7 +7,10 @@ import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
// import Grid from "@mui/material/Grid2";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
|
||||
export default async function ExplorerPage() {
|
||||
// Resolve once on the server and pass IDs to client components
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { countryCodeMap } from "@/assets/countryCodes";
|
||||
import { addSeconds } from "date-fns";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
CountryDataResponse,
|
||||
CurrentEpochData,
|
||||
ExplorerData,
|
||||
GatewayStatus,
|
||||
IAccountBalancesInfo,
|
||||
IObservatoryNode,
|
||||
IPacketsAndStakingData,
|
||||
NS_NODE,
|
||||
NodeRewardDetails,
|
||||
NymTokenomics,
|
||||
ObservatoryBalance,
|
||||
@@ -14,10 +16,10 @@ import type {
|
||||
import {
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_BALANCES_URL,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
SPECTREDAO_BALANCES_URL,
|
||||
NS_API_NODES,
|
||||
NYM_ACCOUNT_ADDRESS,
|
||||
NYM_PRICES_API,
|
||||
SPECTREDAO_NYM_PRICES_API,
|
||||
OBSERVATORY_GATEWAYS_URL,
|
||||
} from "./urls";
|
||||
|
||||
@@ -42,7 +44,7 @@ export const fetchEpochRewards = async (): Promise<
|
||||
|
||||
// Fetch gateway status based on identity key
|
||||
export const fetchGatewayStatus = async (
|
||||
identityKey: string,
|
||||
identityKey: string
|
||||
): Promise<GatewayStatus | null> => {
|
||||
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
|
||||
|
||||
@@ -54,17 +56,14 @@ export const fetchGatewayStatus = async (
|
||||
};
|
||||
|
||||
export const fetchNodeDelegations = async (
|
||||
id: number,
|
||||
id: number
|
||||
): Promise<NodeRewardDetails[]> => {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
const response = await fetch(`${NS_API_NODES}/${id}/delegations`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch delegations");
|
||||
@@ -89,7 +88,7 @@ export const fetchCurrentEpoch = async () => {
|
||||
const data: CurrentEpochData = await response.json();
|
||||
const epochEndTime = addSeconds(
|
||||
new Date(data.current_epoch_start),
|
||||
data.epoch_length.secs,
|
||||
data.epoch_length.secs
|
||||
).toISOString();
|
||||
|
||||
return { ...data, current_epoch_end: epochEndTime };
|
||||
@@ -97,7 +96,7 @@ export const fetchCurrentEpoch = async () => {
|
||||
|
||||
// Fetch balances based on the address
|
||||
export const fetchBalances = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
const response = await fetch(`${SPECTREDAO_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
@@ -118,10 +117,8 @@ export const fetchBalances = async (address: string): Promise<number> => {
|
||||
};
|
||||
|
||||
// Fetch function to get total staker rewards
|
||||
export const fetchTotalStakerRewards = async (
|
||||
address: string,
|
||||
): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
export const fetchTotalStakerRewards = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${SPECTREDAO_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
@@ -140,7 +137,7 @@ export const fetchTotalStakerRewards = async (
|
||||
|
||||
// Fetch function to get the original stake
|
||||
export const fetchOriginalStake = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
const response = await fetch(`${SPECTREDAO_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
@@ -160,7 +157,7 @@ export const fetchOriginalStake = async (address: string): Promise<number> => {
|
||||
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
||||
if (!process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS) {
|
||||
throw new Error(
|
||||
"NEXT_PUBLIC_NS_API_MIXNODES_STATS environment variable is not defined",
|
||||
"NEXT_PUBLIC_NS_API_MIXNODES_STATS environment variable is not defined"
|
||||
);
|
||||
}
|
||||
const response = await fetch(process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS, {
|
||||
@@ -176,7 +173,7 @@ export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
||||
|
||||
// Fetch Account Balance
|
||||
export const fetchAccountBalance = async (
|
||||
address: string,
|
||||
address: string
|
||||
): Promise<IAccountBalancesInfo> => {
|
||||
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
|
||||
headers: {
|
||||
@@ -192,42 +189,9 @@ export const fetchAccountBalance = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchObservatoryNodes = async (): Promise<IObservatoryNode[]> => {
|
||||
const allNodes: IObservatoryNode[] = [];
|
||||
let page = 1;
|
||||
const PAGE_SIZE = 200;
|
||||
let hasMoreData = true;
|
||||
|
||||
while (hasMoreData) {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}?page=${page}&limit=${PAGE_SIZE}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch observatory nodes (page ${page})`);
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
allNodes.push(...nodes);
|
||||
|
||||
if (nodes.length < PAGE_SIZE) {
|
||||
hasMoreData = false; // Stop fetching when the last page has fewer than 200 items
|
||||
} else {
|
||||
page++; // Move to the next page
|
||||
}
|
||||
}
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
// 🔹 Fetch NYM Price
|
||||
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||
const res = await fetch(NYM_PRICES_API, {
|
||||
const res = await fetch(SPECTREDAO_NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
@@ -239,3 +203,190 @@ export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||
const data: NymTokenomics = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchNSApiNodes = async (): Promise<NS_NODE[]> => {
|
||||
if (!NS_API_NODES) {
|
||||
throw new Error("NS_API_NODES URL is not defined");
|
||||
}
|
||||
|
||||
const allNodes: any[] = [];
|
||||
let page = 0;
|
||||
const PAGE_SIZE = 200;
|
||||
let totalItems = 0;
|
||||
let hasMoreData = true;
|
||||
|
||||
while (hasMoreData) {
|
||||
const response = await fetch(
|
||||
`${NS_API_NODES}?page=${page}&size=${PAGE_SIZE}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch NS API nodes (page ${page}): ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const nodes: any[] = data.items || [];
|
||||
allNodes.push(...nodes);
|
||||
|
||||
// Get total count from response
|
||||
totalItems = data.total || 0;
|
||||
|
||||
// Check if we've fetched all items
|
||||
if (allNodes.length >= totalItems) {
|
||||
hasMoreData = false;
|
||||
} else {
|
||||
page++; // Move to the next page
|
||||
}
|
||||
}
|
||||
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
export const fetchWorldMapCountries = async (): Promise<{
|
||||
countries: CountryDataResponse;
|
||||
totalCountries: number;
|
||||
uniqueLocations: number;
|
||||
totalServers: number;
|
||||
}> => {
|
||||
// Fetch all nodes from the NS API
|
||||
const nodes = await fetchNSApiNodes();
|
||||
|
||||
// Create a map to count nodes by country
|
||||
const countryCounts: Record<string, number> = {};
|
||||
// Set to track unique cities
|
||||
const uniqueCities = new Set<string>();
|
||||
|
||||
// Process each node
|
||||
for (const node of nodes) {
|
||||
// Get the 2-letter country code from the node's geoip data
|
||||
const twoLetterCode = node.geoip?.country;
|
||||
|
||||
if (twoLetterCode) {
|
||||
// Convert to 3-letter country code
|
||||
const threeLetterCode = countryCodeMap[twoLetterCode] || twoLetterCode;
|
||||
|
||||
// Increment the count for this country
|
||||
countryCounts[threeLetterCode] =
|
||||
(countryCounts[threeLetterCode] || 0) + 1;
|
||||
|
||||
// Add city to unique cities set if it exists
|
||||
if (node.geoip?.city) {
|
||||
uniqueCities.add(node.geoip.city);
|
||||
}
|
||||
} else {
|
||||
// If no geoip data, count it as unknown
|
||||
countryCounts[""] = (countryCounts[""] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the counts to the required format
|
||||
const result: CountryDataResponse = {};
|
||||
|
||||
for (const [threeLetterCode, count] of Object.entries(countryCounts)) {
|
||||
result[threeLetterCode] = {
|
||||
ISO3: threeLetterCode,
|
||||
nodes: count,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
countries: result,
|
||||
totalCountries: Object.keys(countryCounts).length,
|
||||
uniqueLocations: uniqueCities.size,
|
||||
totalServers: nodes.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRecommendedNodes = (nodes: NS_NODE[]): number[] => {
|
||||
function toNumber(x: unknown, fallback = 0): number {
|
||||
const n =
|
||||
typeof x === "string" || typeof x === "number" ? Number(x) : Number.NaN;
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
const MIN_STAKE = 50_000_000_000; // 50k NYM (uNYM)
|
||||
const MAX_STAKE = 150_000_000_000; // 150k NYM (uNYM)
|
||||
const MAX_PM = 0.2; // ≤ 20%
|
||||
const MIN_UPTIME = 0.95; // ≥ 95%
|
||||
|
||||
// require gateway roles: entry + exit_ipr + exit_nr; NOT a mixnode
|
||||
function hasRequiredRoles(n: NS_NODE): boolean {
|
||||
const r = n.self_description?.declared_role;
|
||||
if (!r) return false;
|
||||
const mixnodeFalse = r.mixnode === false || r.mixnode === undefined;
|
||||
return mixnodeFalse && !!r.entry && !!r.exit_ipr && !!r.exit_nr;
|
||||
}
|
||||
|
||||
function hasGoodPM(n: NS_NODE): boolean {
|
||||
const pm = toNumber(
|
||||
n.rewarding_details?.cost_params?.profit_margin_percent,
|
||||
Number.NaN
|
||||
);
|
||||
return !Number.isNaN(pm) && pm <= MAX_PM;
|
||||
}
|
||||
|
||||
function stakeInRange(n: NS_NODE): boolean {
|
||||
const s = toNumber(n.total_stake, 0);
|
||||
return s > MIN_STAKE && s < MAX_STAKE;
|
||||
}
|
||||
|
||||
function meetsUptime(n: NS_NODE): boolean {
|
||||
const u = toNumber(n.uptime, -1);
|
||||
return u >= MIN_UPTIME;
|
||||
}
|
||||
|
||||
function wireguardOn(n: NS_NODE): boolean {
|
||||
return n.self_description?.wireguard != null;
|
||||
}
|
||||
|
||||
function sortByUptimeDescStakeAsc(a: NS_NODE, b: NS_NODE): number {
|
||||
const ua = toNumber(a.uptime, 0);
|
||||
const ub = toNumber(b.uptime, 0);
|
||||
if (ub !== ua) return ub - ua; // higher uptime first
|
||||
const sa = toNumber(a.total_stake, 0);
|
||||
const sb = toNumber(b.total_stake, 0);
|
||||
return sa - sb; // then lower stake first
|
||||
}
|
||||
const baseFilter = (n: NS_NODE) =>
|
||||
(n.bonded === true || n.bonded === undefined) &&
|
||||
hasRequiredRoles(n) &&
|
||||
hasGoodPM(n) &&
|
||||
stakeInRange(n) &&
|
||||
meetsUptime(n); // uptime hard floor
|
||||
|
||||
// prefer wg-enabled nodes first
|
||||
const wgCandidates = nodes
|
||||
.filter((n) => baseFilter(n) && wireguardOn(n))
|
||||
.sort(sortByUptimeDescStakeAsc);
|
||||
|
||||
let picked = wgCandidates.slice(0, 10);
|
||||
|
||||
// if fewer than 10, drop wg pref but keep base filter
|
||||
if (picked.length < 10) {
|
||||
const relaxed = nodes.filter(baseFilter).sort(sortByUptimeDescStakeAsc);
|
||||
const have = new Set(picked.map((n) => n.node_id));
|
||||
for (const n of relaxed) {
|
||||
if (have.size >= 10) break;
|
||||
const id =
|
||||
typeof n.node_id === "number" ? n.node_id : toNumber(n.node_id, 0);
|
||||
if (!have.has(id)) {
|
||||
picked = [...picked, n];
|
||||
have.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return picked
|
||||
.map((n) =>
|
||||
typeof n.node_id === "number" ? n.node_id : toNumber(n.node_id, 0)
|
||||
)
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
};
|
||||
|
||||
Vendored
-23417
File diff suppressed because it is too large
Load Diff
@@ -72,56 +72,59 @@ export interface ExplorerData {
|
||||
}
|
||||
|
||||
export type NodeDescription = {
|
||||
last_polled: string;
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
auxiliary_details: {
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
mix_port: number | null;
|
||||
verloc_port: number | null;
|
||||
};
|
||||
location: string;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
commit_branch: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
rustc_channel: string;
|
||||
rustc_version: string;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
ip_address: string[];
|
||||
hostname: string;
|
||||
ip_address: [string, string];
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
declared_role: {
|
||||
mixnode: boolean;
|
||||
entry: boolean;
|
||||
exit_nr: boolean;
|
||||
exit_ipr: boolean;
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
auxiliary_details: {
|
||||
location: string;
|
||||
announce_ports: {
|
||||
verloc_port: number | null;
|
||||
mix_port: number | null;
|
||||
};
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
commit_branch: string;
|
||||
rustc_version: string;
|
||||
rustc_channel: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
wireguard: string | null;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
wireguard: {
|
||||
port: number;
|
||||
public_key: string;
|
||||
};
|
||||
} | null;
|
||||
|
||||
@@ -165,15 +168,6 @@ export type Location = {
|
||||
longitude?: number;
|
||||
};
|
||||
|
||||
export type NodeData = {
|
||||
node_id: number;
|
||||
contract_node_type: string;
|
||||
description: NodeDescription;
|
||||
bond_information: BondInformation;
|
||||
rewarding_details: RewardingDetails;
|
||||
location: Location;
|
||||
};
|
||||
|
||||
// ACCOUNT BALANCES
|
||||
|
||||
export interface IRewardDetails {
|
||||
@@ -207,111 +201,15 @@ export interface IAccountBalancesInfo {
|
||||
vesting_account?: null | string;
|
||||
}
|
||||
|
||||
export interface IObservatoryNode {
|
||||
accepted_tnc: boolean;
|
||||
bonded: boolean;
|
||||
bonding_address: string;
|
||||
description: {
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
auxiliary_details: {
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
mix_port: number | null;
|
||||
verloc_port: number | null;
|
||||
};
|
||||
location: string | null;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
commit_branch: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
rustc_channel: string;
|
||||
rustc_version: string;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
hostname: string | null;
|
||||
ip_address: string[];
|
||||
};
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
wireguard: string | null;
|
||||
geoip: {
|
||||
city: string;
|
||||
country: string;
|
||||
ip_address: string;
|
||||
loc: string;
|
||||
node_id: number;
|
||||
org: string;
|
||||
postal: string;
|
||||
region: string;
|
||||
};
|
||||
};
|
||||
identity_key: string;
|
||||
ip_address: string;
|
||||
node_id: number;
|
||||
node_type: string;
|
||||
original_pledge: number;
|
||||
rewarding_details: {
|
||||
cost_params: {
|
||||
interval_operating_cost: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
profit_margin_percent: string;
|
||||
};
|
||||
delegates: string;
|
||||
last_rewarded_epoch: number;
|
||||
operator: string;
|
||||
total_unit_reward: string;
|
||||
unique_delegations: number;
|
||||
unit_delegation: string;
|
||||
};
|
||||
self_description: {
|
||||
details: string;
|
||||
moniker: string;
|
||||
security_contact: string;
|
||||
website: string;
|
||||
};
|
||||
total_stake: number;
|
||||
uptime: number;
|
||||
}
|
||||
export interface NodeRewardDetails {
|
||||
amount: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
block_height: number;
|
||||
cumulative_reward_ratio: string;
|
||||
height: number;
|
||||
node_id: number;
|
||||
owner: string;
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
export type LastProbeResult = {
|
||||
@@ -480,3 +378,58 @@ export type NymTokenomics = {
|
||||
symbol: string;
|
||||
total_supply: number;
|
||||
};
|
||||
|
||||
export type CountryData = {
|
||||
ISO3: string;
|
||||
nodes: number;
|
||||
};
|
||||
|
||||
export interface CountryDataResponse {
|
||||
[threeLetterCountryCode: string]: CountryData;
|
||||
}
|
||||
|
||||
export type NS_NODE = {
|
||||
accepted_tnc: boolean;
|
||||
bonded: boolean;
|
||||
bonding_address: string;
|
||||
description: {
|
||||
details: string;
|
||||
moniker: string;
|
||||
security_contact: string;
|
||||
website: string;
|
||||
};
|
||||
geoip?: {
|
||||
city: string;
|
||||
country: string;
|
||||
ip_address: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
org: string;
|
||||
postal: string;
|
||||
region: string;
|
||||
timezone: string;
|
||||
};
|
||||
identity_key: string;
|
||||
ip_address: string;
|
||||
node_id: number;
|
||||
node_type: string;
|
||||
original_pledge: number;
|
||||
rewarding_details?: {
|
||||
cost_params: {
|
||||
interval_operating_cost: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
profit_margin_percent: string;
|
||||
};
|
||||
delegates: string;
|
||||
last_rewarded_epoch: number;
|
||||
operator: string;
|
||||
total_unit_reward: string;
|
||||
unique_delegations: number;
|
||||
unit_delegation: string;
|
||||
} | null;
|
||||
self_description?: NodeDescription;
|
||||
total_stake: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
@@ -5,15 +5,11 @@ export const CURRENT_EPOCH_REWARDS =
|
||||
|
||||
export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
|
||||
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
||||
export const SPECTREDAO_NYM_PRICES_API =
|
||||
"https://api.nym.spectredao.net/api/v1/nym-price";
|
||||
export const VALIDATOR_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
||||
export const DATA_OBSERVATORY_NODES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/nodes";
|
||||
|
||||
export const DATA_OBSERVATORY_DELEGATIONS_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/delegations";
|
||||
export const DATA_OBSERVATORY_BALANCES_URL =
|
||||
export const SPECTREDAO_BALANCES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/balances";
|
||||
export const OBSERVATORY_GATEWAYS_URL =
|
||||
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const TABLET_WIDTH = "(min-width:700px)";
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// API
|
||||
import { client } from "../../../lib/strapiClient";
|
||||
|
||||
// Types
|
||||
import type { Languages } from "../../../i18n";
|
||||
|
||||
import type { components } from "@/app/lib/strapi";
|
||||
// Constants
|
||||
import { bannerApiPath } from "../../banner/config/constants";
|
||||
|
||||
// Fetch footer data
|
||||
export const getBanner = async (
|
||||
locale: Languages,
|
||||
): Promise<{
|
||||
id?: number;
|
||||
attributes?: components["schemas"]["ExplorerBanner"];
|
||||
} | null> => {
|
||||
const banner = await client.GET(bannerApiPath, {
|
||||
params: {
|
||||
query: {
|
||||
locale,
|
||||
// @ts-expect-error - populate is not typed correctly?
|
||||
populate: {
|
||||
links: {
|
||||
populate: "*",
|
||||
},
|
||||
icon: {
|
||||
populate: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return banner?.data?.data ? banner?.data?.data : null;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const bannerApiPath = "/explorer-banner";
|
||||
@@ -1,72 +0,0 @@
|
||||
// Types
|
||||
import type { components } from "../../../lib/strapi";
|
||||
|
||||
// Components
|
||||
import { Link } from "@/components/muiLink";
|
||||
|
||||
// MUI Components
|
||||
import { Box, Grid2, Typography } from "@mui/material";
|
||||
|
||||
export const FooterLinks = ({
|
||||
linkBlocks = [],
|
||||
}: {
|
||||
linkBlocks: components["schemas"]["FooterLinkBlockComponent"][];
|
||||
}) => {
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
spacing={{ xs: 2, md: 3 }}
|
||||
columns={{ xs: 1, sm: 8, md: 5 }}
|
||||
>
|
||||
{linkBlocks?.map((block) => {
|
||||
return (
|
||||
<Grid2 key={block.id} size={{ xs: 1, sm: 4, md: 1 }}>
|
||||
<Typography
|
||||
component={block?.heading?.level || "h3"}
|
||||
variant="subtitle1"
|
||||
sx={{ mb: 4 }}
|
||||
>
|
||||
{block?.heading?.title}
|
||||
</Typography>
|
||||
<Box
|
||||
component={"ul"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{block?.links?.map((link) => {
|
||||
const isLinkExternal = link.url?.startsWith("http");
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
listStyle: "none",
|
||||
}}
|
||||
component={"li"}
|
||||
key={link.id}
|
||||
>
|
||||
<Link
|
||||
href={link?.url || ""}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body3">
|
||||
{link.title}
|
||||
{isLinkExternal ? " ↗" : ""}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Grid2>
|
||||
);
|
||||
})}
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Banner } from "@/components/banner/Banner";
|
||||
import { Header } from "@/components/header";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import Providers from "@/providers";
|
||||
@@ -21,6 +22,7 @@ export default function RootLayout({
|
||||
<body>
|
||||
<Providers>
|
||||
<Header />
|
||||
<Banner />
|
||||
<Wrapper>{children}</Wrapper>
|
||||
<Footer />
|
||||
</Providers>
|
||||
|
||||
Vendored
+35223
-22138
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,14 @@
|
||||
import { WorldMap } from "@/components/worldMap/WorldMap";
|
||||
import { Stack } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../components/headings/SectionHeading";
|
||||
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
|
||||
import { StakersNumberCard } from "../components/landingPageComponents/StakersNumberCard";
|
||||
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
|
||||
import { CurrentEpochCardWrapper } from "../components/landingPageComponents/CurrentEpochCardWrapper";
|
||||
import { NetworkStakeCardWrapper } from "../components/landingPageComponents/NetworkStakeCardWrapper";
|
||||
import { NoiseCardWrapper } from "../components/landingPageComponents/NoiseCardWrapper";
|
||||
import { StakersNumberCardWrapper } from "../components/landingPageComponents/StakersNumberCardWrapper";
|
||||
import { TokenomicsCardWrapper } from "../components/landingPageComponents/TokenomicsCardWrapper";
|
||||
import NodeTable from "../components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
|
||||
|
||||
@@ -16,38 +17,29 @@ export default async function Home() {
|
||||
<ContentLayout>
|
||||
<Stack gap={5}>
|
||||
<NodeAndAddressSearch />
|
||||
<WorldMap />
|
||||
</Stack>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Noise Generating Network Overview" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<NoiseCard />
|
||||
<SectionHeading title="Network Overview" />
|
||||
</Grid>
|
||||
<NoiseCardWrapper />
|
||||
<Grid
|
||||
container
|
||||
columnSpacing={5}
|
||||
rowSpacing={5}
|
||||
size={{ xs: 12, sm: 6, lg: 3 }}
|
||||
>
|
||||
<Grid size={12}>
|
||||
<StakersNumberCard />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<CurrentEpochCard />
|
||||
</Grid>
|
||||
<StakersNumberCardWrapper />
|
||||
<CurrentEpochCardWrapper />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<NetworkStakeCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
<NetworkStakeCardWrapper />
|
||||
<TokenomicsCardWrapper />
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid container rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Nym Nodes" />
|
||||
<SectionHeading title="Nym Servers" />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<NodeTable />
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// Map of 2-letter country codes to 3-letter country codes
|
||||
export const countryCodeMap: Record<string, string> = {
|
||||
AF: "AFG", // Afghanistan
|
||||
AL: "ALB", // Albania
|
||||
DZ: "DZA", // Algeria
|
||||
AD: "AND", // Andorra
|
||||
AO: "AGO", // Angola
|
||||
AG: "ATG", // Antigua and Barbuda
|
||||
AR: "ARG", // Argentina
|
||||
AM: "ARM", // Armenia
|
||||
AU: "AUS", // Australia
|
||||
AT: "AUT", // Austria
|
||||
AZ: "AZE", // Azerbaijan
|
||||
BS: "BHS", // Bahamas
|
||||
BH: "BHR", // Bahrain
|
||||
BD: "BGD", // Bangladesh
|
||||
BB: "BRB", // Barbados
|
||||
BY: "BLR", // Belarus
|
||||
BE: "BEL", // Belgium
|
||||
BZ: "BLZ", // Belize
|
||||
BJ: "BEN", // Benin
|
||||
BT: "BTN", // Bhutan
|
||||
BO: "BOL", // Bolivia
|
||||
BA: "BIH", // Bosnia and Herzegovina
|
||||
BW: "BWA", // Botswana
|
||||
BR: "BRA", // Brazil
|
||||
BN: "BRN", // Brunei
|
||||
BG: "BGR", // Bulgaria
|
||||
BF: "BFA", // Burkina Faso
|
||||
BI: "BDI", // Burundi
|
||||
KH: "KHM", // Cambodia
|
||||
CM: "CMR", // Cameroon
|
||||
CA: "CAN", // Canada
|
||||
CV: "CPV", // Cape Verde
|
||||
CF: "CAF", // Central African Republic
|
||||
TD: "TCD", // Chad
|
||||
CL: "CHL", // Chile
|
||||
CN: "CHN", // China
|
||||
CO: "COL", // Colombia
|
||||
KM: "COM", // Comoros
|
||||
CG: "COG", // Congo
|
||||
CR: "CRI", // Costa Rica
|
||||
HR: "HRV", // Croatia
|
||||
CU: "CUB", // Cuba
|
||||
CY: "CYP", // Cyprus
|
||||
CZ: "CZE", // Czech Republic
|
||||
DK: "DNK", // Denmark
|
||||
DJ: "DJI", // Djibouti
|
||||
DM: "DMA", // Dominica
|
||||
DO: "DOM", // Dominican Republic
|
||||
EC: "ECU", // Ecuador
|
||||
EG: "EGY", // Egypt
|
||||
SV: "SLV", // El Salvador
|
||||
GQ: "GNQ", // Equatorial Guinea
|
||||
ER: "ERI", // Eritrea
|
||||
EE: "EST", // Estonia
|
||||
ET: "ETH", // Ethiopia
|
||||
FJ: "FJI", // Fiji
|
||||
FI: "FIN", // Finland
|
||||
FR: "FRA", // France
|
||||
GA: "GAB", // Gabon
|
||||
GM: "GMB", // Gambia
|
||||
GE: "GEO", // Georgia
|
||||
DE: "DEU", // Germany
|
||||
GH: "GHA", // Ghana
|
||||
GR: "GRC", // Greece
|
||||
GD: "GRD", // Grenada
|
||||
GT: "GTM", // Guatemala
|
||||
GN: "GIN", // Guinea
|
||||
GW: "GNB", // Guinea-Bissau
|
||||
GY: "GUY", // Guyana
|
||||
HT: "HTI", // Haiti
|
||||
HN: "HND", // Honduras
|
||||
HU: "HUN", // Hungary
|
||||
IS: "ISL", // Iceland
|
||||
IN: "IND", // India
|
||||
ID: "IDN", // Indonesia
|
||||
IR: "IRN", // Iran
|
||||
IQ: "IRQ", // Iraq
|
||||
IE: "IRL", // Ireland
|
||||
IL: "ISR", // Israel
|
||||
IT: "ITA", // Italy
|
||||
JM: "JAM", // Jamaica
|
||||
JP: "JPN", // Japan
|
||||
JO: "JOR", // Jordan
|
||||
KZ: "KAZ", // Kazakhstan
|
||||
KE: "KEN", // Kenya
|
||||
KI: "KIR", // Kiribati
|
||||
KP: "PRK", // North Korea
|
||||
KR: "KOR", // South Korea
|
||||
KW: "KWT", // Kuwait
|
||||
KG: "KGZ", // Kyrgyzstan
|
||||
LA: "LAO", // Laos
|
||||
LV: "LVA", // Latvia
|
||||
LB: "LBN", // Lebanon
|
||||
LS: "LSO", // Lesotho
|
||||
LR: "LBR", // Liberia
|
||||
LY: "LBY", // Libya
|
||||
LI: "LIE", // Liechtenstein
|
||||
LT: "LTU", // Lithuania
|
||||
LU: "LUX", // Luxembourg
|
||||
MG: "MDG", // Madagascar
|
||||
MW: "MWI", // Malawi
|
||||
MY: "MYS", // Malaysia
|
||||
MV: "MDV", // Maldives
|
||||
ML: "MLI", // Mali
|
||||
MT: "MLT", // Malta
|
||||
MH: "MHL", // Marshall Islands
|
||||
MR: "MRT", // Mauritania
|
||||
MU: "MUS", // Mauritius
|
||||
MX: "MEX", // Mexico
|
||||
FM: "FSM", // Micronesia
|
||||
MD: "MDA", // Moldova
|
||||
MC: "MCO", // Monaco
|
||||
MN: "MNG", // Mongolia
|
||||
ME: "MNE", // Montenegro
|
||||
MA: "MAR", // Morocco
|
||||
MZ: "MOZ", // Mozambique
|
||||
MM: "MMR", // Myanmar
|
||||
NA: "NAM", // Namibia
|
||||
NR: "NRU", // Nauru
|
||||
NP: "NPL", // Nepal
|
||||
NL: "NLD", // Netherlands
|
||||
NZ: "NZL", // New Zealand
|
||||
NI: "NIC", // Nicaragua
|
||||
NE: "NER", // Niger
|
||||
NG: "NGA", // Nigeria
|
||||
NO: "NOR", // Norway
|
||||
OM: "OMN", // Oman
|
||||
PK: "PAK", // Pakistan
|
||||
PW: "PLW", // Palau
|
||||
PA: "PAN", // Panama
|
||||
PG: "PNG", // Papua New Guinea
|
||||
PY: "PRY", // Paraguay
|
||||
PE: "PER", // Peru
|
||||
PH: "PHL", // Philippines
|
||||
PL: "POL", // Poland
|
||||
PT: "PRT", // Portugal
|
||||
QA: "QAT", // Qatar
|
||||
RO: "ROU", // Romania
|
||||
RU: "RUS", // Russia
|
||||
RW: "RWA", // Rwanda
|
||||
KN: "KNA", // Saint Kitts and Nevis
|
||||
LC: "LCA", // Saint Lucia
|
||||
VC: "VCT", // Saint Vincent and the Grenadines
|
||||
WS: "WSM", // Samoa
|
||||
SM: "SMR", // San Marino
|
||||
ST: "STP", // Sao Tome and Principe
|
||||
SA: "SAU", // Saudi Arabia
|
||||
SN: "SEN", // Senegal
|
||||
RS: "SRB", // Serbia
|
||||
SC: "SYC", // Seychelles
|
||||
SL: "SLE", // Sierra Leone
|
||||
SG: "SGP", // Singapore
|
||||
SK: "SVK", // Slovakia
|
||||
SI: "SVN", // Slovenia
|
||||
SB: "SLB", // Solomon Islands
|
||||
SO: "SOM", // Somalia
|
||||
ZA: "ZAF", // South Africa
|
||||
SS: "SSD", // South Sudan
|
||||
ES: "ESP", // Spain
|
||||
LK: "LKA", // Sri Lanka
|
||||
SD: "SDN", // Sudan
|
||||
SR: "SUR", // Suriname
|
||||
SZ: "SWZ", // Swaziland
|
||||
SE: "SWE", // Sweden
|
||||
CH: "CHE", // Switzerland
|
||||
SY: "SYR", // Syria
|
||||
TW: "TWN", // Taiwan
|
||||
TJ: "TJK", // Tajikistan
|
||||
TZ: "TZA", // Tanzania
|
||||
TH: "THA", // Thailand
|
||||
TL: "TLS", // Timor-Leste
|
||||
TG: "TGO", // Togo
|
||||
TO: "TON", // Tonga
|
||||
TT: "TTO", // Trinidad and Tobago
|
||||
TN: "TUN", // Tunisia
|
||||
TR: "TUR", // Turkey
|
||||
TM: "TKM", // Turkmenistan
|
||||
TV: "TUV", // Tuvalu
|
||||
UG: "UGA", // Uganda
|
||||
UA: "UKR", // Ukraine
|
||||
AE: "ARE", // United Arab Emirates
|
||||
GB: "GBR", // United Kingdom
|
||||
US: "USA", // United States
|
||||
UY: "URY", // Uruguay
|
||||
UZ: "UZB", // Uzbekistan
|
||||
VU: "VUT", // Vanuatu
|
||||
VA: "VAT", // Vatican City
|
||||
VE: "VEN", // Venezuela
|
||||
VN: "VNM", // Vietnam
|
||||
YE: "YEM", // Yemen
|
||||
ZM: "ZMB", // Zambia
|
||||
ZW: "ZWE", // Zimbabwe
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { IRewardDetails } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -61,6 +61,8 @@ const calculateStakingRewards = (
|
||||
|
||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
const { address } = props;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const {
|
||||
data: accountInfo,
|
||||
@@ -101,7 +103,10 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
if (isError || priceError || !accountInfo || !nymPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance } from "@/app/api";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
@@ -13,6 +13,8 @@ interface IAccountInfoCardProps {
|
||||
|
||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
const { address } = props;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
@@ -38,7 +40,10 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Total NYM">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -9,18 +10,20 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function AccountPageButtonGroup({ address }: Props) {
|
||||
const { data: nymNodes, isError } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (!nymNodes || isError) return null;
|
||||
if (!nsApiNodes || isNSApiNodesError) return null;
|
||||
|
||||
const nymNode = nymNodes.find((node) => node.bonding_address === address);
|
||||
const nymNode = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.bonding_address === address,
|
||||
);
|
||||
|
||||
if (!nymNode) return null;
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { getBanner } from "@/app/features/banner/api/getBanner";
|
||||
import type { components } from "@/app/lib/strapi";
|
||||
import { Close, Launch } from "@mui/icons-material";
|
||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "../muiLink";
|
||||
import { Wrapper } from "../wrapper";
|
||||
|
||||
type BannerData = {
|
||||
id?: number;
|
||||
attributes?: components["schemas"]["ExplorerBanner"];
|
||||
} | null;
|
||||
|
||||
export const Banner = () => {
|
||||
const [bannerData, setBannerData] = useState<BannerData>(null);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBanner = async () => {
|
||||
try {
|
||||
const data = await getBanner("en");
|
||||
setBannerData(data);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch banner:", error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBanner();
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
// Don't render if not visible
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only show banner if API data is available and show is true
|
||||
const shouldShowBanner = bannerData?.attributes?.show === true;
|
||||
const hasValidData = bannerData?.attributes && shouldShowBanner;
|
||||
|
||||
// If loading and no data yet, don't show anything
|
||||
if (isLoading && !bannerData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If API data says don't show and we have valid data, don't show
|
||||
if (hasValidData && !shouldShowBanner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no valid data from API, don't show anything
|
||||
if (!hasValidData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { title, text, links, icon } = bannerData.attributes || {};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "accent.main",
|
||||
color: "base.black",
|
||||
py: { xs: 1.5, md: 2 },
|
||||
borderBottom: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Wrapper>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
alignItems={{ xs: "flex-start", md: "center" }}
|
||||
justifyContent="space-between"
|
||||
spacing={{ xs: 1.5, md: 2 }}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={3}
|
||||
flex={1}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{/* Icon */}
|
||||
{icon?.data?.attributes?.url && (
|
||||
<Box
|
||||
component="img"
|
||||
src={icon.data.attributes.url}
|
||||
alt={icon.data.attributes.alternativeText || "Banner icon"}
|
||||
sx={{
|
||||
width: { xs: 20, md: 24 },
|
||||
height: { xs: 20, md: 24 },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<Box flex={1}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 0.5,
|
||||
fontSize: { xs: "0.875rem", md: "1rem" },
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
opacity: 0.9,
|
||||
fontSize: { xs: "0.75rem", md: "0.875rem" },
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Actions */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{
|
||||
alignSelf: { xs: "flex-end", md: "center" },
|
||||
width: { xs: "auto", md: "auto" },
|
||||
}}
|
||||
>
|
||||
{/* Links */}
|
||||
{links && links.length > 0 && (
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={1}
|
||||
sx={{ flexWrap: "wrap" }}
|
||||
>
|
||||
{links.map((link) => (
|
||||
<Link
|
||||
key={link.id}
|
||||
href={link.url || "#"}
|
||||
target={link.url?.startsWith("http") ? "_blank" : "_self"}
|
||||
rel={
|
||||
link.url?.startsWith("http") ? "noopener noreferrer" : ""
|
||||
}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
endIcon={
|
||||
link.url?.startsWith("http") ? <Launch /> : undefined
|
||||
}
|
||||
sx={{
|
||||
color: "base.black",
|
||||
borderColor: "base.black",
|
||||
fontSize: { xs: "0.75rem", md: "0.875rem" },
|
||||
px: { xs: 1, md: 2 },
|
||||
py: { xs: 0.5, md: 1 },
|
||||
"&:hover": {
|
||||
borderColor: "base.black",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
sx={{
|
||||
color: "base.black",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Close sx={{ fontSize: { xs: "1.25rem", md: "1.5rem" } }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,6 @@ const BlogArticlesCards = async ({
|
||||
const blogArticle = JSON.parse(fileContent);
|
||||
return {
|
||||
...blogArticle,
|
||||
link: `/onboarding/${filename.replace(".json", "")}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ type BlogArticle = {
|
||||
image: string;
|
||||
iconLight: string;
|
||||
iconDark: string;
|
||||
link: string;
|
||||
attributes: {
|
||||
blogAuthors: string[];
|
||||
date: Date;
|
||||
@@ -23,8 +24,6 @@ type BlogArticle = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type BlogArticleWithLink = BlogArticle & {
|
||||
link: string;
|
||||
};
|
||||
export type BlogArticleWithLink = BlogArticle;
|
||||
|
||||
export default BlogArticle;
|
||||
|
||||
@@ -49,9 +49,9 @@ const ExplorerHeroCard = ({
|
||||
|
||||
const dynamicCardStyles = {
|
||||
...cardStyles,
|
||||
bgcolor: isDarkMode ? "pine.300" : "background.paper",
|
||||
bgcolor: isDarkMode ? "#EFFFF0" : "background.paper",
|
||||
"&:hover": {
|
||||
bgcolor: isDarkMode ? "pine.600" : "accent.main",
|
||||
bgcolor: isDarkMode ? "#C2FFC7" : "#E5E7EB",
|
||||
},
|
||||
...sx,
|
||||
};
|
||||
@@ -68,7 +68,12 @@ const ExplorerHeroCard = ({
|
||||
const iconSrc = isDarkMode ? iconDarkSrc : iconLightSrc;
|
||||
|
||||
return (
|
||||
<Link href={link} sx={{ textDecoration: "none", height: "100%" }}>
|
||||
<Link
|
||||
href={link}
|
||||
sx={{ textDecoration: "none", height: "100%" }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Card sx={dynamicCardStyles} elevation={0}>
|
||||
<CardHeader
|
||||
title={
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import { IconButton, Typography, useTheme } from "@mui/material";
|
||||
import { useCopyToClipboard } from "@uidotdev/usehooks";
|
||||
import { useEffect } from "react";
|
||||
@@ -34,12 +35,8 @@ const CopyToClipboard = ({
|
||||
|
||||
if (hasCopied) {
|
||||
return (
|
||||
<Typography
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
variant="h6"
|
||||
color="textSecondary"
|
||||
>
|
||||
Copied
|
||||
<Typography sx={{ color: isDarkMode ? "base.white" : "pine.950" }}>
|
||||
<CheckIcon fontSize="small" />
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SocialChannels = () => {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
color: "background.main",
|
||||
backgroundColor: "light.main",
|
||||
backgroundColor: "secondary.main",
|
||||
"&:hover": {
|
||||
backgroundColor: "background.default",
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function Footer() {
|
||||
const locale = "en";
|
||||
const footerData = await getFooter(locale);
|
||||
const legalContent1 =
|
||||
"Nym Noise Generating Network Explorer, V 2.1.0 Public Beta release.";
|
||||
"Nym Noise Generating Network Explorer, V 2.2.0 Public Beta release.";
|
||||
const legalContent2 = footerData?.attributes?.legalContent2 || false;
|
||||
const footerLinkBlocks = footerData?.attributes?.linkBlocks || [];
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { DesktopHeader } from "./DesktopHeader";
|
||||
import { MobileHeader } from "./MobileHeader";
|
||||
|
||||
export const Header = async () => {
|
||||
return (
|
||||
<header>
|
||||
<Box
|
||||
component="header"
|
||||
sx={{
|
||||
backgroundColor: "background.default",
|
||||
}}
|
||||
>
|
||||
<DesktopHeader />
|
||||
<MobileHeader />
|
||||
{/* Mobile header will go here */}
|
||||
</header>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface ConditionalCardWrapperProps {
|
||||
children: ReactNode;
|
||||
size?:
|
||||
| number
|
||||
| { xs?: number; sm?: number; md?: number; lg?: number; xl?: number };
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export const ConditionalCardWrapper = ({
|
||||
children,
|
||||
size,
|
||||
visible = true,
|
||||
}: ConditionalCardWrapperProps) => {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Grid size={size}>{children}</Grid>;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type EpochResponseData,
|
||||
useEpochContext,
|
||||
} from "@/providers/EpochProvider";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { differenceInMinutes, format } from "date-fns";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -34,6 +34,9 @@ export const CurrentEpochCard = () => {
|
||||
const [endTime, setEndTime] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const updateState = useCallback((data: NonNullable<EpochResponseData>) => {
|
||||
const { startTime, endTime } = getStartEndTime(
|
||||
data.current_epoch_start,
|
||||
@@ -65,30 +68,28 @@ export const CurrentEpochCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography variant="body3" fontWeight="light">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography variant="body3" fontWeight="light">
|
||||
No data available
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (epochStatus === "pending") {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography variant="body3" fontWeight="light" height={80}>
|
||||
<Typography
|
||||
variant="body3"
|
||||
fontWeight="light"
|
||||
height={80}
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Waiting for next epoch to start...
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
import { useEpochContext } from "@/providers/EpochProvider";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
import { CurrentEpochCard } from "./CurrentEpochCard";
|
||||
|
||||
export const CurrentEpochCardWrapper = () => {
|
||||
const { data, isError, isLoading, epochStatus } = useEpochContext();
|
||||
|
||||
// Determine if the card should be visible
|
||||
// Show the card if we have data and it's not in a pending state, or if we're still loading
|
||||
const isVisible =
|
||||
!isError && (data || isLoading) && epochStatus !== "pending";
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={12} visible={isVisible}>
|
||||
<CurrentEpochCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -33,21 +33,14 @@ export const NetworkStakeCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isStakingError || !packetsAndStaking) {
|
||||
return (
|
||||
<ExplorerCard label="Current network stake">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
// Don't display the card if there's an error or insufficient data
|
||||
if (
|
||||
isStakingError ||
|
||||
!packetsAndStaking ||
|
||||
!Array.isArray(packetsAndStaking) ||
|
||||
packetsAndStaking.length < 10
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packetsAndStakingData: ExplorerData["packetsAndStakingData"] =
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
import { fetchNoise } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
import { NetworkStakeCard } from "./NetworkStakeCard";
|
||||
|
||||
export const NetworkStakeCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length >= 10;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<NetworkStakeCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -41,31 +41,21 @@ export const NoiseCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Mixnet traffic">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
// Don't display the card if there's an error or insufficient data
|
||||
if (isError || !data || !Array.isArray(data) || data.length < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const todaysData = data[data.length - 2];
|
||||
const yesterdaysData = data[data.length - 3];
|
||||
|
||||
const noiseLast24H =
|
||||
todaysData.total_packets_sent + todaysData.total_packets_received;
|
||||
(todaysData?.total_packets_sent || 0) +
|
||||
(todaysData?.total_packets_received || 0);
|
||||
|
||||
const noisePrevious24H =
|
||||
yesterdaysData.total_packets_sent + yesterdaysData.total_packets_received;
|
||||
(yesterdaysData?.total_packets_sent || 0) +
|
||||
(yesterdaysData?.total_packets_received || 0);
|
||||
|
||||
const formatNoiseVolume = (packets: number): string => {
|
||||
if (packets < 0) {
|
||||
@@ -107,8 +97,9 @@ export const NoiseCard = () => {
|
||||
.slice(0, -1)
|
||||
.map((item: IPacketsAndStakingData) => {
|
||||
return {
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
date_utc: item?.date_utc,
|
||||
numericData:
|
||||
(item?.total_packets_sent || 0) + (item?.total_packets_received || 0),
|
||||
};
|
||||
})
|
||||
.filter((item) => item.numericData >= 2_500_000_000);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
import { fetchNoise } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
import { NoiseCard } from "./NoiseCard";
|
||||
|
||||
export const NoiseCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length >= 10;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<NoiseCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const StakersNumberCard = () => {
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: () => fetchObservatoryNodes(),
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -22,7 +22,7 @@ export const StakersNumberCard = () => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isLoading) {
|
||||
if (isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
<Skeleton variant="text" height={90} />
|
||||
@@ -30,11 +30,11 @@ export const StakersNumberCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !nymNodes) {
|
||||
if (isNSApiNodesError || !nsApiNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
<Typography
|
||||
variant="h3"
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
@@ -43,13 +43,13 @@ export const StakersNumberCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const getActiveStakersNumber = (nodes: IObservatoryNode[]): number => {
|
||||
const getActiveStakersNumber = (nodes: NS_NODE[]): number => {
|
||||
return nodes.reduce(
|
||||
(sum, node) => sum + node.rewarding_details.unique_delegations,
|
||||
(sum, node) => sum + (node.rewarding_details?.unique_delegations || 0),
|
||||
0,
|
||||
);
|
||||
};
|
||||
const allStakers = getActiveStakersNumber(nymNodes);
|
||||
const allStakers = getActiveStakersNumber(nsApiNodes);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
import { StakersNumberCard } from "./StakersNumberCard";
|
||||
|
||||
export const StakersNumberCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length > 0;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={12} visible={isVisible}>
|
||||
<StakersNumberCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ExplorerData, NymTokenomics } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -9,6 +9,8 @@ import ExplorerListItem from "../list/ListItem";
|
||||
import { TitlePrice } from "../price/TitlePrice";
|
||||
|
||||
export const TokenomicsCard = () => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading,
|
||||
@@ -69,7 +71,10 @@ export const TokenomicsCard = () => {
|
||||
) {
|
||||
return (
|
||||
<ExplorerCard label="Tokenomics overview">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load tokenomics overview.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={80} />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
import { TokenomicsCard } from "./TokenomicsCard";
|
||||
|
||||
export const TokenomicsCardWrapper = () => {
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading: isPriceLoading,
|
||||
isError: isPriceError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymPrice"],
|
||||
queryFn: fetchNymPrice,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: epochRewards,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: packetsAndStaking,
|
||||
isLoading: isStakingLoading,
|
||||
isError: isStakingError,
|
||||
} = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isLoading = isPriceLoading || isEpochLoading || isStakingLoading;
|
||||
const hasError = isPriceError || isEpochError || isStakingError;
|
||||
const hasData =
|
||||
nymPrice &&
|
||||
epochRewards &&
|
||||
packetsAndStaking &&
|
||||
Array.isArray(packetsAndStaking) &&
|
||||
packetsAndStaking.length >= 2;
|
||||
|
||||
const isVisible = !hasError && (hasData || isLoading);
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<TokenomicsCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
"use client";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Slider,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React from "react";
|
||||
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import FilterAltIcon from "@mui/icons-material/FilterAlt";
|
||||
import PercentIcon from "@mui/icons-material/Percent";
|
||||
import PieChartIcon from "@mui/icons-material/PieChart";
|
||||
import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup";
|
||||
|
||||
type AdvancedFiltersProps = {
|
||||
uptime: [number, number];
|
||||
setUptime: (value: [number, number]) => void;
|
||||
saturation: [number, number];
|
||||
setSaturation: (value: [number, number]) => void;
|
||||
profitMargin: [number, number];
|
||||
setProfitMargin: (value: [number, number]) => void;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
maxSaturation?: number;
|
||||
activeFilter: "all" | "mixnodes" | "gateways" | "recommended";
|
||||
setActiveFilter: (
|
||||
filter: "all" | "mixnodes" | "gateways" | "recommended",
|
||||
) => void;
|
||||
nodeCounts: {
|
||||
all: number;
|
||||
mixnodes: number;
|
||||
gateways: number;
|
||||
};
|
||||
/** Count of recommended nodes (passed from server) */
|
||||
recommendedCount: number;
|
||||
};
|
||||
|
||||
export default function AdvancedFilters({
|
||||
uptime,
|
||||
setUptime,
|
||||
saturation,
|
||||
setSaturation,
|
||||
profitMargin,
|
||||
setProfitMargin,
|
||||
open,
|
||||
setOpen,
|
||||
maxSaturation = 100,
|
||||
activeFilter,
|
||||
setActiveFilter,
|
||||
nodeCounts,
|
||||
recommendedCount,
|
||||
}: AdvancedFiltersProps) {
|
||||
const theme = useTheme();
|
||||
const green = "#14e76f"; // from theme colours
|
||||
|
||||
const marksPercent: { value: number }[] = [{ value: 0 }, { value: 100 }];
|
||||
const marksSaturation: { value: number }[] = [
|
||||
{ value: 0 },
|
||||
{ value: maxSaturation },
|
||||
];
|
||||
|
||||
const panel = (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontStyle: "italic",
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.common.black
|
||||
: theme.palette.common.white,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
Advanced filtering mode is active
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<AccessTimeIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Uptime
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{uptime[0]}% - {uptime[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={uptime}
|
||||
onChange={(_, v) => setUptime(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={100}
|
||||
marks={marksPercent}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<PieChartIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Saturation
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{saturation[0]}% - {saturation[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={saturation}
|
||||
onChange={(_, v) => setSaturation(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={maxSaturation}
|
||||
marks={marksSaturation}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} sx={{ mx: { sm: "auto" } }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<PercentIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Profit Margin
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{profitMargin[0]}% - {profitMargin[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={profitMargin}
|
||||
onChange={(_, v) => setProfitMargin(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={100}
|
||||
marks={marksPercent}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: { xs: "100%", sm: "auto" } }}>
|
||||
<NodeFilterButtonGroup
|
||||
size="medium"
|
||||
options={[
|
||||
{
|
||||
label: `Recommended servers (${recommendedCount})`,
|
||||
isSelected: activeFilter === "recommended",
|
||||
value: "recommended",
|
||||
},
|
||||
{
|
||||
label: `All servers (${nodeCounts.all})`,
|
||||
isSelected: activeFilter === "all",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: `Mixnodes (${nodeCounts.mixnodes})`,
|
||||
isSelected: activeFilter === "mixnodes",
|
||||
value: "mixnodes",
|
||||
},
|
||||
{
|
||||
label: `Gateways (${nodeCounts.gateways})`,
|
||||
isSelected: activeFilter === "gateways",
|
||||
value: "gateways",
|
||||
},
|
||||
]}
|
||||
onPage={activeFilter}
|
||||
onFilterChange={setActiveFilter}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
startIcon={
|
||||
<FilterAltIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? `${theme.palette.common.black} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => setOpen?.(!open)}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? `${theme.palette.common.black} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
borderColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.common.black
|
||||
: theme.palette.common.white,
|
||||
background: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: 16,
|
||||
"&:hover, &:focus": {
|
||||
background:
|
||||
theme.palette.mode === "light"
|
||||
? "rgba(0,0,0,0.04)"
|
||||
: "rgba(255,255,255,0.05)",
|
||||
borderColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[400]
|
||||
: theme.palette.common.white,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Advanced Filters
|
||||
</Button>
|
||||
</Box>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{panel}
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
@@ -18,8 +19,10 @@ import {
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { colours } from "@/theme/colours";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite } from "../favorite/Favorite";
|
||||
import Loading from "../loading";
|
||||
@@ -189,54 +192,6 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
Cell: ({ row }) => <Favorite address={row.original.owner} />,
|
||||
},
|
||||
|
||||
{
|
||||
id: "name",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Name</ColumnHeading>,
|
||||
accessorKey: "name",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.name || "-"}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node ID</ColumnHeading>,
|
||||
accessorKey: "nodeId",
|
||||
size: 90,
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.nodeId}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "identity_key",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Identity Key</ColumnHeading>,
|
||||
accessorKey: "identity_key",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body5">{row.original.identity_key}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "qos",
|
||||
header: "Qlt of Service",
|
||||
align: "center",
|
||||
accessorKey: "qualityOfService",
|
||||
size: 100,
|
||||
Header: <ColumnHeading>Qlt of Service</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.qualityOfService.toFixed()}%
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "location",
|
||||
header: "Location",
|
||||
@@ -255,6 +210,98 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
header: "",
|
||||
size: 210,
|
||||
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
accessorKey: "name",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.name || "-"}</Typography>
|
||||
<Tooltip
|
||||
title={row.original.identity_key}
|
||||
placement="bottom"
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: "none",
|
||||
whiteSpace: "nowrap",
|
||||
bgcolor: isDarkMode ? "#374042" : "#E5E7EB",
|
||||
color: isDarkMode ? "#FFFFFF" : "#000000",
|
||||
"& .MuiTooltip-arrow": {
|
||||
color: isDarkMode ? "#374042" : "#E5E7EB",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{ height: "24px" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body5"
|
||||
sx={{
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{row.original.identity_key.length > 17
|
||||
? `${row.original.identity_key.slice(0, 10)}...${row.original.identity_key.slice(-8)}`
|
||||
: row.original.identity_key}
|
||||
</Typography>
|
||||
<CopyToClipboard text={row.original.identity_key} />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node ID</ColumnHeading>,
|
||||
accessorKey: "nodeId",
|
||||
size: 90,
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.nodeId}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "qos",
|
||||
header: "Qlt of Service",
|
||||
align: "center",
|
||||
accessorKey: "qualityOfService",
|
||||
size: 100,
|
||||
Header: <ColumnHeading>Uptime</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.qualityOfService;
|
||||
let color = "#000000";
|
||||
|
||||
if (value >= 80) {
|
||||
color = "#22C55E"; // green
|
||||
} else if (value >= 50) {
|
||||
color = "#F59E0B"; // amber/orange-yellow
|
||||
} else {
|
||||
color = "#EF4444"; // red
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
|
||||
{value.toFixed()}%
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "stakeSaturation",
|
||||
header: "Stake saturation",
|
||||
@@ -262,9 +309,58 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
size: 120,
|
||||
|
||||
Header: <ColumnHeading>Saturation</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.stakeSaturation;
|
||||
let color = "#000000";
|
||||
|
||||
if (value > 100) {
|
||||
color = "#EF4444";
|
||||
} else if (value >= 75) {
|
||||
color = "#22C55E";
|
||||
} else if (value >= 25) {
|
||||
color = "#F59E0B";
|
||||
} else {
|
||||
color = "#EF4444";
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
|
||||
{value}%
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "selfBond",
|
||||
header: "Self-bond",
|
||||
accessorKey: "selfBond",
|
||||
Header: <ColumnHeading>Self-bond</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.selfBond;
|
||||
let color = isDarkMode ? "#FFFFFF" : "#000000";
|
||||
|
||||
if (value === 0) {
|
||||
color = "#EF4444";
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ color, fontWeight: value === 0 ? 400 : 300 }}
|
||||
>
|
||||
{row.original.selfBond} NYM
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "operatingCosts",
|
||||
header: "Operating costs",
|
||||
accessorKey: "operatingCosts",
|
||||
Header: <ColumnHeading>Operating costs</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.stakeSaturation}%
|
||||
{row.original.operatingCosts} NYM
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
@@ -303,7 +399,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[isWalletConnected, handleOnSelectStake, favorites],
|
||||
[isWalletConnected, handleOnSelectStake, favorites, isDarkMode],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
@@ -342,9 +438,10 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
bgcolor: isDarkMode ? "#0F1720" : "background.paper",
|
||||
},
|
||||
},
|
||||
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor: isDarkMode ? "#374042" : "background.paper",
|
||||
bgcolor: isDarkMode ? "background.default" : "background.paper",
|
||||
},
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
@@ -410,20 +507,29 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
onClick: () => {
|
||||
router.push(`/nym-node/${row.original.nodeId}`);
|
||||
onClick: (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
window.open(`/explorer/nym-node/${row.original.nodeId}`, "_blank");
|
||||
} else {
|
||||
router.push(`/nym-node/${row.original.nodeId}`);
|
||||
}
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
backgroundColor: isDarkMode
|
||||
? row.index % 2 === 0
|
||||
? "#3E4A4C !important"
|
||||
: "#374042 !important"
|
||||
: row.index % 2 === 0
|
||||
? "#F3F7FB"
|
||||
: "white",
|
||||
":nth-child(even)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? `${colours.pine[950]} !important`
|
||||
: `${colours.base.white} !important`,
|
||||
},
|
||||
":nth-child(odd)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? `${colours.pine[800]} !important`
|
||||
: `${colours.haze[25]} !important`,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: `${isDarkMode ? "#2A3436" : "#E5E7EB"} !important`,
|
||||
backgroundColor: `${theme.palette.mode === "dark" ? "#004449" : "#E5E7EB"} !important`,
|
||||
transition: "background-color 0.2s ease",
|
||||
},
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -3,61 +3,150 @@
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
fetchEpochRewards,
|
||||
fetchNSApiNodes,
|
||||
getRecommendedNodes,
|
||||
} from "../../app/api";
|
||||
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||
import { countryName } from "../../utils/countryName";
|
||||
import AdvancedFilters from "./AdvancedFilters";
|
||||
import NodeTable from "./NodeTable";
|
||||
|
||||
// Utility function to calculate node saturation point
|
||||
|
||||
|
||||
function getNodeSaturationPoint(
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string,
|
||||
): number {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
throw new Error("Invalid stake saturation point provided");
|
||||
}
|
||||
|
||||
const ratio = (totalStake / saturation) * 100;
|
||||
|
||||
return Number(ratio.toFixed());
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
const mappedNSApiNodes = (
|
||||
nodes: NS_NODE[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
) =>
|
||||
nodes.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
nodes
|
||||
.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
+node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||
/&/g,
|
||||
"&",
|
||||
);
|
||||
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
countryName:
|
||||
countryName(node.description.auxiliary_details.location) || null,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
qualityOfService: +node.uptime * 100,
|
||||
};
|
||||
});
|
||||
const selfBondFormatted = node.original_pledge
|
||||
? Number(node.original_pledge) / 1_000_000
|
||||
: 0;
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
const operatingCostsFormatted = node.rewarding_details
|
||||
? Number(
|
||||
node.rewarding_details.cost_params.interval_operating_cost.amount,
|
||||
) / 1_000_000
|
||||
: 0;
|
||||
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.geoip?.country || null,
|
||||
countryName: countryName(node.geoip?.country || null) || null,
|
||||
selfBond: selfBondFormatted,
|
||||
operatingCosts: operatingCostsFormatted,
|
||||
profitMarginPercentage: node.rewarding_details
|
||||
? +node.rewarding_details.cost_params.profit_margin_percent * 100
|
||||
: 0,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
qualityOfService: +node.uptime * 100,
|
||||
mixnode: node.self_description?.declared_role.mixnode === true,
|
||||
gateway:
|
||||
node.self_description?.declared_role.entry === true ||
|
||||
node.self_description?.declared_role.exit_ipr === true ||
|
||||
node.self_description?.declared_role.exit_nr === true,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!a.countryName && !b.countryName) return 0;
|
||||
if (!a.countryName) return 1;
|
||||
if (!b.countryName) return -1;
|
||||
return a.countryName.localeCompare(b.countryName);
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const NodeTableWithAction = () => {
|
||||
// All hooks at the top!
|
||||
const [activeFilter, setActiveFilter] = useState<
|
||||
"all" | "mixnodes" | "gateways" | "recommended"
|
||||
>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableActiveFilter");
|
||||
return (
|
||||
(stored as "all" | "mixnodes" | "gateways" | "recommended") ||
|
||||
"recommended"
|
||||
);
|
||||
});
|
||||
const [uptime, setUptime] = useState<[number, number]>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableUptime");
|
||||
return stored ? JSON.parse(stored) : [0, 100];
|
||||
});
|
||||
const [saturation, setSaturation] = useState<[number, number]>([0, 100]);
|
||||
const [profitMargin, setProfitMargin] = useState<[number, number]>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableProfitMargin");
|
||||
return stored ? JSON.parse(stored) : [0, 100];
|
||||
});
|
||||
const [advancedOpen, setAdvancedOpen] = useState(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableAdvancedOpen");
|
||||
return stored ? JSON.parse(stored) : false;
|
||||
});
|
||||
|
||||
// Wrapper functions to handle filter changes and sessionStorage
|
||||
const handleActiveFilterChange = (
|
||||
newFilter: "all" | "mixnodes" | "gateways" | "recommended"
|
||||
) => {
|
||||
setActiveFilter(newFilter);
|
||||
sessionStorage.setItem("nodeTableActiveFilter", newFilter);
|
||||
};
|
||||
|
||||
const handleUptimeChange = (newUptime: [number, number]) => {
|
||||
setUptime(newUptime);
|
||||
sessionStorage.setItem("nodeTableUptime", JSON.stringify(newUptime));
|
||||
};
|
||||
|
||||
const handleSaturationChange = (newSaturation: [number, number]) => {
|
||||
setSaturation(newSaturation);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableSaturation",
|
||||
JSON.stringify(newSaturation)
|
||||
);
|
||||
};
|
||||
|
||||
const handleProfitMarginChange = (newProfitMargin: [number, number]) => {
|
||||
setProfitMargin(newProfitMargin);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableProfitMargin",
|
||||
JSON.stringify(newProfitMargin)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdvancedOpenChange = (newAdvancedOpen: boolean) => {
|
||||
setAdvancedOpen(newAdvancedOpen);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableAdvancedOpen",
|
||||
JSON.stringify(newAdvancedOpen)
|
||||
);
|
||||
};
|
||||
|
||||
// Use React Query to fetch epoch rewards
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
@@ -67,27 +156,57 @@ const NodeTableWithAction = () => {
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const recommendedIds = getRecommendedNodes(nsApiNodes);
|
||||
|
||||
// Map nodes with rewards data
|
||||
const nsApiNodesData = epochRewardsData
|
||||
? mappedNSApiNodes(nsApiNodes || [], epochRewardsData)
|
||||
: [];
|
||||
|
||||
// Calculate max saturation from all nodes
|
||||
const maxSaturation = Math.max(
|
||||
100,
|
||||
...nsApiNodesData.map((n) => n.stakeSaturation || 0)
|
||||
);
|
||||
|
||||
// Initialize saturation from sessionStorage or set to maxSaturation when data is loaded
|
||||
useEffect(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableSaturation");
|
||||
if (stored) {
|
||||
setSaturation(JSON.parse(stored));
|
||||
} else if (nsApiNodesData.length > 0) {
|
||||
setSaturation([0, maxSaturation]);
|
||||
}
|
||||
}, [maxSaturation, nsApiNodesData.length]);
|
||||
|
||||
// Calculate node counts for each type
|
||||
const nodeCounts = {
|
||||
all: nsApiNodesData.length,
|
||||
mixnodes: nsApiNodesData.filter((node) => node.mixnode).length,
|
||||
gateways: nsApiNodesData.filter((node) => node.gateway).length,
|
||||
};
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
@@ -101,7 +220,7 @@ const NodeTableWithAction = () => {
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
if (isEpochError || isNSApiNodesError) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
@@ -111,15 +230,70 @@ const NodeTableWithAction = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
|
||||
if (!epochRewardsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
// Step 1: Filter nodes by type
|
||||
const typeFilteredNodes = nsApiNodesData.filter((node) => {
|
||||
switch (activeFilter) {
|
||||
case "mixnodes":
|
||||
return node.mixnode;
|
||||
case "gateways":
|
||||
return node.gateway;
|
||||
case "recommended":
|
||||
return recommendedIds.includes(node.nodeId);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return <NodeTable nodes={data} />;
|
||||
// Step 2: Apply advanced filters if open (but only if sliders moved from defaults)
|
||||
const isDefault = {
|
||||
uptime: uptime[0] === 0 && uptime[1] === 100,
|
||||
saturation: saturation[0] === 0 && saturation[1] === maxSaturation,
|
||||
profitMargin: profitMargin[0] === 0 && profitMargin[1] === 100,
|
||||
};
|
||||
const filteredNodes = advancedOpen
|
||||
? typeFilteredNodes.filter((node) => {
|
||||
const uptimeMatch =
|
||||
isDefault.uptime ||
|
||||
(node.qualityOfService >= uptime[0] &&
|
||||
node.qualityOfService <= uptime[1]);
|
||||
const saturationMatch =
|
||||
isDefault.saturation ||
|
||||
(node.stakeSaturation >= saturation[0] &&
|
||||
node.stakeSaturation <= saturation[1]);
|
||||
const profitMarginMatch =
|
||||
isDefault.profitMargin ||
|
||||
(node.profitMarginPercentage >= profitMargin[0] &&
|
||||
node.profitMarginPercentage <= profitMargin[1]);
|
||||
return uptimeMatch && saturationMatch && profitMarginMatch;
|
||||
})
|
||||
: typeFilteredNodes;
|
||||
|
||||
const recommendedCount = recommendedIds.length;
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<AdvancedFilters
|
||||
open={advancedOpen}
|
||||
setOpen={handleAdvancedOpenChange}
|
||||
uptime={uptime}
|
||||
setUptime={handleUptimeChange}
|
||||
saturation={saturation}
|
||||
setSaturation={handleSaturationChange}
|
||||
profitMargin={profitMargin}
|
||||
setProfitMargin={handleProfitMarginChange}
|
||||
maxSaturation={maxSaturation}
|
||||
activeFilter={activeFilter}
|
||||
setActiveFilter={handleActiveFilterChange}
|
||||
nodeCounts={nodeCounts}
|
||||
recommendedCount={recommendedCount}
|
||||
/>
|
||||
<NodeTable nodes={filteredNodes} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTableWithAction;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
@@ -14,22 +14,25 @@ type Props = {
|
||||
};
|
||||
|
||||
export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Skeleton variant="text" height={90} />
|
||||
@@ -42,10 +45,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !nymNodes) {
|
||||
if (!nsApiNodes || isNSApiNodesError) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -55,16 +61,20 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const selfBond = formatBigNum(
|
||||
Number(nodeInfo.rewarding_details.operator) / 1_000_000,
|
||||
);
|
||||
const selfBond = nodeInfo.original_pledge
|
||||
? formatBigNum(Number(nodeInfo.original_pledge) / 1_000_000)
|
||||
: 0;
|
||||
const selfBondFormatted = `${selfBond} NYM`;
|
||||
|
||||
return (
|
||||
@@ -85,9 +95,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
|
||||
>
|
||||
{nodeInfo.bonding_address}
|
||||
{nodeInfo.bonding_address
|
||||
? nodeInfo.bonding_address
|
||||
: "Node not bonded"}
|
||||
</Typography>
|
||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||
{nodeInfo.bonding_address && (
|
||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
@@ -113,12 +127,14 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakers"
|
||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||
/>
|
||||
{nodeInfo.rewarding_details && (
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakers"
|
||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||
/>
|
||||
)}
|
||||
<ExplorerListItem row label="Self bonded" value={selfBondFormatted} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { fetchNodeDelegations } from "@/app/api";
|
||||
import { colours } from "@/theme/colours";
|
||||
import { Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ type Props = {
|
||||
const DelegationsTable = ({ id }: Props) => {
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const { data: delegations = [], isError } = useQuery({
|
||||
queryKey: ["nodeDelegations", id],
|
||||
@@ -55,7 +57,7 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
accessorKey: "height",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.height}</Typography>
|
||||
<Typography variant="body4">{row.original.block_height}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
@@ -125,10 +127,7 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "background.paper",
|
||||
bgcolor: isDarkMode ? "background.default" : "background.paper",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -143,25 +142,23 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
":nth-child(odd)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255, 255, 255, 0.05) !important"
|
||||
: "#F3F7FB !important",
|
||||
},
|
||||
":nth-child(even)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? "transparent !important"
|
||||
: "white !important",
|
||||
? `${colours.pine[950]} !important`
|
||||
: `${colours.base.white} !important`,
|
||||
},
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
":nth-child(odd)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1) !important"
|
||||
: "rgba(0, 0, 0, 0.04) !important",
|
||||
? `${colours.pine[800]} !important`
|
||||
: `${colours.haze[25]} !important`,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: `${theme.palette.mode === "dark" ? "#004449" : "#E5E7EB"} !important`,
|
||||
transition: "background-color 0.2s ease",
|
||||
},
|
||||
cursor: "pointer",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeDataCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
@@ -30,19 +30,22 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (isEpochLoading || isLoading) {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -53,10 +56,13 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
||||
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -66,17 +72,23 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const softwareUpdateTime = format(
|
||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
const softwareUpdateTime = nodeInfo.self_description
|
||||
? format(
|
||||
new Date(nodeInfo.self_description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
)
|
||||
: "N/A";
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
@@ -90,13 +102,21 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeInfo.description.host_information.ip_address.toString()}
|
||||
value={
|
||||
nodeInfo.self_description
|
||||
? nodeInfo.self_description.host_information.ip_address.toString()
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeInfo.description.build_information.build_version}
|
||||
value={
|
||||
nodeInfo.self_description
|
||||
? nodeInfo.self_description.build_information.build_version
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import DelegationsTable from "./DelegationsTable";
|
||||
@@ -12,15 +12,17 @@ type Props = {
|
||||
};
|
||||
|
||||
const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
|
||||
const {
|
||||
data: nymNodes,
|
||||
isError,
|
||||
isLoading,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -28,16 +30,20 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
});
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const id = nodeInfo.node_id;
|
||||
|
||||
if (isLoading) {
|
||||
if (isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -48,10 +54,13 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (isNSApiNodesError) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load delegations. Please try again later.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -10,25 +10,29 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function NodePageButtonGroup({ paramId }: Props) {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
|
||||
const { data: nymNodes, isError } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (!nymNodes || isError) return null;
|
||||
if (!nsApiNodes || isNSApiNodesError) return null;
|
||||
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { IObservatoryNode, RewardingDetails } from "../../app/api/types";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import type { NS_NODE } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -13,7 +13,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
|
||||
// Fetch epoch rewards
|
||||
const {
|
||||
@@ -31,19 +33,19 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (isEpochLoading || isLoading) {
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -54,10 +56,13 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
||||
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -66,9 +71,13 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
@@ -77,22 +86,25 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
const totalStakeFormatted = `${totalStake} NYM`;
|
||||
|
||||
// Extract reward details
|
||||
const rewardDetails: RewardingDetails = nodeInfo.rewarding_details;
|
||||
|
||||
const profitMarginPercent =
|
||||
Number(rewardDetails.cost_params.profit_margin_percent) * 100;
|
||||
const profitMarginPercent = nodeInfo.rewarding_details
|
||||
? Number(nodeInfo.rewarding_details.cost_params.profit_margin_percent) * 100
|
||||
: 0;
|
||||
const profitMarginPercentFormated = `${profitMarginPercent}%`;
|
||||
|
||||
const operatingCosts =
|
||||
Number(rewardDetails.cost_params.interval_operating_cost.amount) /
|
||||
1_000_000;
|
||||
const operatingCosts = nodeInfo.rewarding_details
|
||||
? Number(
|
||||
nodeInfo.rewarding_details.cost_params.interval_operating_cost.amount,
|
||||
) / 1_000_000
|
||||
: 0;
|
||||
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
||||
|
||||
const getNodeSaturationPoint = (
|
||||
totalStake: number,
|
||||
nodeTotalStake: string,
|
||||
stakeSaturationPoint: string,
|
||||
): string => {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
throw new Error("Invalid stake saturation point provided");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import {
|
||||
Box,
|
||||
@@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RandomAvatar } from "react-random-avatars";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -31,7 +31,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
const theme = useTheme();
|
||||
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
@@ -47,12 +47,12 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading: isLoadingNymNodes,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -62,9 +62,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId,
|
||||
);
|
||||
} else {
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId),
|
||||
);
|
||||
}
|
||||
|
||||
const handleOnSelectStake = useCallback(() => {
|
||||
@@ -96,7 +100,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
}
|
||||
}, [isWalletConnected, nodeInfo]);
|
||||
|
||||
if (isLoadingNymNodes) {
|
||||
if (isNSApiNodesLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="rectangular" height={80} width={80} />
|
||||
@@ -105,11 +109,11 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
if (isError || !nymNodes) {
|
||||
if (isNSApiNodesError || !nsApiNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: theme.palette.mode === "dark" ? "base.white" : "pine.950",
|
||||
}}
|
||||
@@ -164,12 +168,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
const cleanMoniker = DOMPurify.sanitize(nodeInfo.description.moniker).replace(
|
||||
/&/g,
|
||||
"&",
|
||||
);
|
||||
|
||||
const cleanDescription = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.details,
|
||||
nodeInfo.description.details,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
// get full country name
|
||||
@@ -197,7 +202,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
>
|
||||
{cleanMoniker || "Moniker"}
|
||||
</Typography>
|
||||
{nodeInfo.description.auxiliary_details.location && (
|
||||
{nodeInfo.geoip?.country && (
|
||||
<Box display={"flex"} gap={1}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
@@ -210,10 +215,8 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={nodeInfo.description.auxiliary_details.location}
|
||||
countryName={countryName(
|
||||
nodeInfo.description.auxiliary_details.location,
|
||||
)}
|
||||
countryCode={nodeInfo.geoip?.country || ""}
|
||||
countryName={countryName(nodeInfo.geoip?.country || "")}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Chip, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Chip, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchEpochRewards,
|
||||
fetchGatewayStatus,
|
||||
fetchObservatoryNodes,
|
||||
fetchNSApiNodes,
|
||||
} from "../../app/api";
|
||||
import type {
|
||||
IObservatoryNode,
|
||||
LastProbeResult,
|
||||
NS_NODE,
|
||||
NodeDescription,
|
||||
} from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -148,21 +148,23 @@ function calculateWireguardPerformance(probeResult: LastProbeResult): number {
|
||||
}
|
||||
|
||||
export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
@@ -177,19 +179,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
});
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nsApiNodes.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nsApiNodes.find((node) => node.node_id === Number(paramId));
|
||||
} // Extract node roles once `nodeInfo` is available
|
||||
const nodeRoles = nodeInfo
|
||||
? getNodeRoles(nodeInfo.description.declared_role)
|
||||
|
||||
const nodeRoles = nodeInfo?.self_description
|
||||
? getNodeRoles(nodeInfo.self_description.declared_role)
|
||||
: [];
|
||||
|
||||
// Define whether to fetch gateway status
|
||||
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
|
||||
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
|
||||
);
|
||||
|
||||
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
|
||||
const { data: gatewayStatus } = useQuery({
|
||||
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
|
||||
@@ -200,7 +202,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
if (isLoading || isEpochLoading) {
|
||||
if (isNSApiNodesLoading || isEpochLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Node role & performance">
|
||||
<Skeleton variant="text" height={70} />
|
||||
@@ -210,15 +212,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !nymNodes || !epochRewardsData || isEpochError) {
|
||||
if (isNSApiNodesError || !nsApiNodes || !epochRewardsData || isEpochError) {
|
||||
return (
|
||||
<ExplorerCard label="Node role & performance">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
<Stack key={role} direction="row" gap={1}>
|
||||
@@ -226,8 +232,6 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
</Stack>
|
||||
));
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const qualityOfServiceStars = nodeInfo?.uptime
|
||||
? calculateQualityOfServiceStars(nodeInfo.uptime)
|
||||
: gatewayStatus
|
||||
@@ -247,9 +251,10 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
|
||||
// Function to calculate active set probability
|
||||
const getActiveSetProbability = (
|
||||
totalStake: number,
|
||||
nodeTotalStake: string,
|
||||
stakeSaturationPoint: string,
|
||||
): string => {
|
||||
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -12,22 +13,22 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
|
||||
const NodeAndAddressSearch = () => {
|
||||
const router = useRouter();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchOptions, setSearchOptions] = useState<IObservatoryNode[]>([]);
|
||||
const [searchOptions, setSearchOptions] = useState<NS_NODE[]>([]);
|
||||
|
||||
// Use React Query to fetch nodes
|
||||
const { data: nymNodes = [], isLoading: isLoadingNodes } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
|
||||
const { data: nsApiNodes = [], isLoading: isNSApiNodesLoading } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
@@ -50,7 +51,7 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} catch {
|
||||
setErrorText(
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again.",
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
@@ -58,7 +59,7 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} else {
|
||||
setErrorText(
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again.",
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
@@ -66,9 +67,9 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} else {
|
||||
// Check if it's a node identity key
|
||||
if (nymNodes) {
|
||||
const matchingNode = nymNodes.find(
|
||||
(node) => node.identity_key === inputValue
|
||||
if (nsApiNodes) {
|
||||
const matchingNode = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === inputValue,
|
||||
);
|
||||
|
||||
if (matchingNode) {
|
||||
@@ -77,13 +78,13 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
}
|
||||
setErrorText(
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again.",
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorText(
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again.",
|
||||
);
|
||||
console.error(error);
|
||||
setIsLoading(false); // Stop loading
|
||||
@@ -92,7 +93,7 @@ const NodeAndAddressSearch = () => {
|
||||
|
||||
// Handle search input change
|
||||
const handleSearchInputChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = event.target.value;
|
||||
setInputValue(value);
|
||||
@@ -104,10 +105,8 @@ const NodeAndAddressSearch = () => {
|
||||
|
||||
// Filter nodes by moniker if input is not empty
|
||||
if (value.trim() !== "") {
|
||||
const filteredNodes = nymNodes.filter((node) =>
|
||||
node.self_description?.moniker
|
||||
?.toLowerCase()
|
||||
.includes(value.toLowerCase())
|
||||
const filteredNodes = nsApiNodes.filter((node: NS_NODE) =>
|
||||
node.description.moniker?.toLowerCase().includes(value.toLowerCase()),
|
||||
);
|
||||
setSearchOptions(filteredNodes);
|
||||
} else {
|
||||
@@ -118,7 +117,7 @@ const NodeAndAddressSearch = () => {
|
||||
// Handle node selection from dropdown
|
||||
const handleNodeSelect = (
|
||||
event: React.SyntheticEvent,
|
||||
value: string | IObservatoryNode | null
|
||||
value: string | NS_NODE | null,
|
||||
) => {
|
||||
if (value && typeof value !== "string") {
|
||||
setIsLoading(true); // Show loading spinner
|
||||
@@ -132,9 +131,9 @@ const NodeAndAddressSearch = () => {
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={searchOptions}
|
||||
getOptionLabel={(option: string | IObservatoryNode) => {
|
||||
getOptionLabel={(option: string | NS_NODE) => {
|
||||
if (typeof option === "string") return option;
|
||||
return option.self_description?.moniker || "";
|
||||
return option.description.moniker || "";
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof option === "string" || typeof value === "string")
|
||||
@@ -146,10 +145,10 @@ const NodeAndAddressSearch = () => {
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
key={`${option.node_id}-${option.self_description?.moniker || ""}`}
|
||||
key={`${option.node_id}-${option.description.moniker || ""}`}
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{option.self_description?.moniker || "Unnamed Node"}
|
||||
{option.description.moniker || "Unnamed Node"}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
@@ -182,7 +181,7 @@ const NodeAndAddressSearch = () => {
|
||||
/>
|
||||
)}
|
||||
onChange={handleNodeSelect}
|
||||
loading={isLoadingNodes}
|
||||
loading={isNSApiNodesLoading}
|
||||
loadingText="Loading nodes..."
|
||||
noOptionsText="No nodes found"
|
||||
slotProps={{
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { colours } from "@/theme/colours";
|
||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
@@ -584,7 +585,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor: isDarkMode ? "#374042" : "background.paper",
|
||||
bgcolor: isDarkMode ? "background.default" : "background.paper",
|
||||
},
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
@@ -655,15 +656,20 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
backgroundColor: isDarkMode
|
||||
? row.index % 2 === 0
|
||||
? "#3E4A4C !important"
|
||||
: "#374042 !important"
|
||||
: row.index % 2 === 0
|
||||
? "#F3F7FB"
|
||||
: "white",
|
||||
":nth-child(even)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? `${colours.pine[950]} !important`
|
||||
: `${colours.base.white} !important`,
|
||||
},
|
||||
":nth-child(odd)": {
|
||||
bgcolor:
|
||||
theme.palette.mode === "dark"
|
||||
? `${colours.pine[800]} !important`
|
||||
: `${colours.haze[25]} !important`,
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: `${isDarkMode ? "#2A3436" : "#E5E7EB"} !important`,
|
||||
backgroundColor: `${theme.palette.mode === "dark" ? "#004449" : "#E5E7EB"} !important`,
|
||||
transition: "background-color 0.2s ease",
|
||||
},
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||
import { countryName } from "../../utils/countryName";
|
||||
import StakeTable from "./StakeTable";
|
||||
|
||||
@@ -24,35 +24,46 @@ function getNodeSaturationPoint(
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
const mappedNSApiNodes = (
|
||||
nodes: NS_NODE[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
) =>
|
||||
nodes.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
nodes
|
||||
.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
+node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||
/&/g,
|
||||
"&",
|
||||
);
|
||||
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
countryName:
|
||||
countryName(node.description.auxiliary_details.location) || null,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: +nodeSaturationPoint || 0,
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.geoip?.country || null,
|
||||
countryName: countryName(node.geoip?.country || null) || null,
|
||||
profitMarginPercentage: node.rewarding_details
|
||||
? +node.rewarding_details.cost_params.profit_margin_percent * 100
|
||||
: 0,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Handle null country names by putting them at the end
|
||||
if (!a.countryName && !b.countryName) return 0;
|
||||
if (!a.countryName) return 1;
|
||||
if (!b.countryName) return -1;
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
// Sort alphabetically by country name
|
||||
return a.countryName.localeCompare(b.countryName);
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const StakeTableWithAction = () => {
|
||||
@@ -72,12 +83,12 @@ const StakeTableWithAction = () => {
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -85,7 +96,7 @@ const StakeTableWithAction = () => {
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
@@ -99,7 +110,7 @@ const StakeTableWithAction = () => {
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
if (isEpochError || isNSApiNodesError) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
@@ -115,9 +126,9 @@ const StakeTableWithAction = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
|
||||
|
||||
return <StakeTable nodes={data} />;
|
||||
return <StakeTable nodes={nsApiNodesData} />;
|
||||
};
|
||||
|
||||
export default StakeTableWithAction;
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import { fetchTotalStakerRewards } from "../../app/api";
|
||||
import type { NodeRewardDetails } from "../../app/api/types";
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import Loading from "../loading";
|
||||
@@ -87,7 +86,7 @@ const SubHeaderRowActions = () => {
|
||||
{ gasPrice },
|
||||
);
|
||||
|
||||
const messages = delegations.map((delegation: NodeRewardDetails) => ({
|
||||
const messages = delegations.map((delegation: Delegation) => ({
|
||||
contractAddress: NYM_MIXNET_CONTRACT,
|
||||
funds: [],
|
||||
msg: {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
import { Button, ButtonGroup, Stack } from "@mui/material";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
isSelected: boolean;
|
||||
value: "all" | "mixnodes" | "gateways" | "recommended";
|
||||
};
|
||||
|
||||
type Options = [Option, Option, Option, Option];
|
||||
|
||||
const NodeFilterButtonGroup = ({
|
||||
size = "small",
|
||||
options,
|
||||
onPage,
|
||||
onFilterChange,
|
||||
}: {
|
||||
size?: "small" | "medium" | "large";
|
||||
options: Options;
|
||||
onPage: string;
|
||||
onFilterChange: (
|
||||
filter: "all" | "mixnodes" | "gateways" | "recommended",
|
||||
) => void;
|
||||
}) => {
|
||||
const handleClick = (
|
||||
value: "all" | "mixnodes" | "gateways" | "recommended",
|
||||
) => {
|
||||
if (onPage === value) return;
|
||||
onFilterChange(value);
|
||||
};
|
||||
|
||||
const getMobileButtonStyles = (isSelected: boolean) => ({
|
||||
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||
"&:hover": {
|
||||
bgcolor: isSelected ? "primary.main" : "",
|
||||
},
|
||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||
width: "100%",
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
});
|
||||
|
||||
const getDesktopButtonStyles = (isSelected: boolean) => ({
|
||||
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||
"&:hover": {
|
||||
bgcolor: isSelected ? "primary.main" : "",
|
||||
},
|
||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile view - Stack */}
|
||||
<Stack
|
||||
spacing={1.5}
|
||||
sx={{
|
||||
display: { xs: "flex", sm: "none" },
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.label}
|
||||
onClick={() => handleClick(option.value)}
|
||||
sx={getMobileButtonStyles(option.isSelected)}
|
||||
variant="outlined"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Desktop view - ButtonGroup */}
|
||||
<ButtonGroup size={size} sx={{ display: { xs: "none", sm: "flex" } }}>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.label}
|
||||
onClick={() => handleClick(option.value)}
|
||||
sx={getDesktopButtonStyles(option.isSelected)}
|
||||
variant="outlined"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeFilterButtonGroup;
|
||||
@@ -0,0 +1,406 @@
|
||||
"use client";
|
||||
|
||||
import { fetchWorldMapCountries } from "@/app/api";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
||||
import { Box, IconButton, Skeleton, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
ZoomableGroup,
|
||||
} from "react-simple-maps";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import type { CountryDataResponse } from "../../app/api/types";
|
||||
import MAP_TOPOJSON from "../../assets/world-110m.json";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const mapPlaceholderDark = "/explorer/map-placeholder-dark.png";
|
||||
const mapPlaceholderLight = "/explorer/map-placeholder-light.png";
|
||||
|
||||
export const WorldMap = (): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const [position, setPosition] = React.useState<{
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}>({ coordinates: [0, 0], zoom: 1 });
|
||||
|
||||
const {
|
||||
data: {
|
||||
countries = [],
|
||||
totalCountries = 0,
|
||||
uniqueLocations = 0,
|
||||
totalServers = 0,
|
||||
} = {
|
||||
countries: [],
|
||||
totalCountries: 0,
|
||||
uniqueLocations: 0,
|
||||
totalServers: 0,
|
||||
},
|
||||
isLoading: isLoadingCountries,
|
||||
isError: isCountriesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodesCountries"],
|
||||
queryFn: fetchWorldMapCountries,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const [tooltipContent, setTooltipContent] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleMouseLeave = () => setTooltipContent("");
|
||||
return () => {
|
||||
handleMouseLeave();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const colorScale = React.useMemo(() => {
|
||||
if (countries) {
|
||||
const heighestNumberOfNodes = Math.max(
|
||||
...Object.values(countries).map((country) => country.nodes),
|
||||
);
|
||||
return scaleLinear<string, string>()
|
||||
.domain([
|
||||
0,
|
||||
1,
|
||||
heighestNumberOfNodes / 4,
|
||||
heighestNumberOfNodes / 2,
|
||||
heighestNumberOfNodes,
|
||||
])
|
||||
.range(
|
||||
isDarkMode
|
||||
? [
|
||||
theme.palette.pine[950],
|
||||
"#0F5A2E", // Dark green
|
||||
"#147A3D", // Medium green
|
||||
"#1A994C", // Light green
|
||||
theme.palette.accent.main,
|
||||
]
|
||||
: [
|
||||
theme.palette.pine[300],
|
||||
"#0F5A2E", // Dark green
|
||||
"#147A3D", // Medium green
|
||||
"#1A994C", // Light green
|
||||
theme.palette.accent.main,
|
||||
],
|
||||
)
|
||||
.unknown(isDarkMode ? theme.palette.pine[950] : theme.palette.pine[25]);
|
||||
}
|
||||
return () =>
|
||||
isDarkMode ? theme.palette.pine[950] : theme.palette.pine[25];
|
||||
}, [countries, theme.palette.pine, theme.palette.accent, isDarkMode]);
|
||||
|
||||
if (isLoadingCountries) {
|
||||
return (
|
||||
<ExplorerCard label="Nym server locations" sx={{ width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
aspectRatio: "16/7",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={isDarkMode ? mapPlaceholderDark : mapPlaceholderLight}
|
||||
alt="World Map Placeholder"
|
||||
fill
|
||||
style={{ objectFit: "contain" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCountriesError) {
|
||||
return (
|
||||
<ExplorerCard label="Nym server locations" sx={{ width: "100%" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={500} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerCard
|
||||
label="Nym server locations"
|
||||
sx={{
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
p: { xs: 2, sm: 3 },
|
||||
"& > .MuiCardContent-root": {
|
||||
height: {
|
||||
xs: "200px",
|
||||
sm: "auto",
|
||||
},
|
||||
aspectRatio: {
|
||||
xs: "unset",
|
||||
sm: "16/7",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? "#000000" : theme.palette.pine[25],
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
viewBox="0 0 800 400"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: 130,
|
||||
}}
|
||||
>
|
||||
<ZoomableGroup
|
||||
center={position.coordinates}
|
||||
zoom={position.zoom}
|
||||
minZoom={1}
|
||||
maxZoom={8}
|
||||
translateExtent={[
|
||||
[-800, -400],
|
||||
[800, 400],
|
||||
]}
|
||||
onMoveEnd={({
|
||||
coordinates,
|
||||
zoom,
|
||||
}: {
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}) => {
|
||||
setPosition({ coordinates, zoom });
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }: { geographies: GeoJSON.Feature[] }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = Array.isArray(countries)
|
||||
? { nodes: 0 }
|
||||
: (countries as CountryDataResponse)[
|
||||
geo.properties?.ISO_A3 as string
|
||||
] || { nodes: 0 };
|
||||
return (
|
||||
<Geography
|
||||
key={`${geo.properties?.ISO_A3 || ""}-${geo.id}-${
|
||||
geo.properties?.NAME_LONG || ""
|
||||
}`}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200]
|
||||
}
|
||||
strokeWidth={0.2}
|
||||
data-tooltip-id="map-tooltip"
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties as {
|
||||
NAME_LONG: string;
|
||||
};
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent("");
|
||||
}}
|
||||
style={{
|
||||
hover: countries
|
||||
? {
|
||||
fill: theme.palette.accent.main,
|
||||
outline: "white",
|
||||
cursor: "pointer",
|
||||
}
|
||||
: undefined,
|
||||
default: {
|
||||
outline: "none",
|
||||
},
|
||||
pressed: {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.min(prev.zoom + 0.5, 8),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.max(prev.zoom - 0.5, 1),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setPosition({ coordinates: [0, 0], zoom: 1 })}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Box>
|
||||
<Tooltip
|
||||
id="map-tooltip"
|
||||
content={tooltipContent}
|
||||
float={true}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "4px 8px",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200],
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.base.white
|
||||
: theme.palette.pine[950],
|
||||
borderRadius: "4px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: {
|
||||
xs: "1fr",
|
||||
sm: "repeat(3, 1fr)",
|
||||
},
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<ExplorerCard label="Nym servers around the world">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalServers}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Countries with Nym servers">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalCountries}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Cities with Nym servers">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{uniqueLocations}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@
|
||||
"iconLight": "explorerCard",
|
||||
"iconDark": "explorerCardDark",
|
||||
"image": "/explorer/images/Network.webp",
|
||||
"link": "https://nym.com/blog/welcome-to-explorer",
|
||||
"overview": {
|
||||
"content": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"iconLight": "stakeCard",
|
||||
"iconDark": "stakeCardDark",
|
||||
"image": "/explorer/images/stake-article.webp",
|
||||
"link": "https://nym.com/blog/stake-Nym-tokens",
|
||||
"overview": {
|
||||
"content": [
|
||||
{
|
||||
|
||||
@@ -5,7 +5,9 @@ export const colours = {
|
||||
error: "#E01400",
|
||||
},
|
||||
green: {
|
||||
500: "#14e76f",
|
||||
300: "#EFFFF0",
|
||||
400: "#C2FFC7",
|
||||
500: "#07FF94",
|
||||
},
|
||||
haze: {
|
||||
25: "#F3F7FB",
|
||||
@@ -17,7 +19,7 @@ export const colours = {
|
||||
200: "#CAD6D7",
|
||||
300: "#A6B9BA",
|
||||
600: "#4C666A",
|
||||
800: "#3E4A4C",
|
||||
800: "#374042",
|
||||
900: "#374042",
|
||||
950: "#242B2D",
|
||||
},
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*.json" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
declare module "react-simple-maps";
|
||||
+1
@@ -0,0 +1 @@
|
||||
declare module "react-tooltip";
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"buildCommand": "yarn build",
|
||||
"installCommand": "yarn install",
|
||||
"framework": "nextjs",
|
||||
"build": { "env": { "NODE_VERSION": "20", "LERNA_USE_NX": "false", "NX_DAEMON": "false" } }
|
||||
}
|
||||
+17
-3
@@ -45,11 +45,18 @@
|
||||
"types:lint:fix": "lerna run lint:fix --scope @nymproject/types --scope @nymproject/nym-wallet-app",
|
||||
"audit:fix": "npm_config_yes=true npx yarn-audit-fix -- --dry-run",
|
||||
"dev:on": "node sdk/typescript/scripts/dev-mode-add.mjs",
|
||||
"dev:off": "node sdk/typescript/scripts/dev-mode-remove.mjs"
|
||||
"dev:off": "node sdk/typescript/scripts/dev-mode-remove.mjs",
|
||||
"security:audit": "yarn audit --level moderate",
|
||||
"security:audit:fix": "yarn audit --fix",
|
||||
"security:audit:ci": "yarn install --frozen-lockfile && yarn audit --level moderate",
|
||||
"security:check": "yarn audit --level high && yarn list --depth=0",
|
||||
"security:outdated": "yarn outdated",
|
||||
"security:verify": "yarn audit --level moderate && yarn list --depth=0 && yarn outdated",
|
||||
"security:full": "./scripts/security-check.sh",
|
||||
"security:ci": "yarn install --frozen-lockfile && ./scripts/security-check.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@npmcli/node-gyp": "^3.0.0",
|
||||
"lerna": "^7.3.0",
|
||||
"node-gyp": "^9.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"tslog": "3.3.3"
|
||||
@@ -62,6 +69,13 @@
|
||||
"@cosmjs/proto-signing": "^0.32.4",
|
||||
"@cosmjs/stargate": "^0.32.4",
|
||||
"@cosmjs/cosmwasm-stargate": "^0.32.4",
|
||||
"cosmjs-types": "^0.9.0"
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"chalk": "5.3.0",
|
||||
"strip-ansi": "7.1.0",
|
||||
"color-convert": "2.0.1",
|
||||
"color-name": "1.1.4",
|
||||
"is-core-module": "2.13.1",
|
||||
"error-ex": "1.3.2",
|
||||
"has-ansi": "5.0.1"
|
||||
}
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "starting security checks..."
|
||||
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "error: package.json not found, please run this script from the project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checking Node.js version..."
|
||||
if [ -f ".nvmrc" ]; then
|
||||
REQUIRED_NODE_VERSION=$(cat .nvmrc)
|
||||
CURRENT_NODE_VERSION=$(node --version | sed 's/v//')
|
||||
echo "required Node.js version: $REQUIRED_NODE_VERSION"
|
||||
echo "current Node.js version: $CURRENT_NODE_VERSION"
|
||||
|
||||
if [ "$CURRENT_NODE_VERSION" != "$REQUIRED_NODE_VERSION" ]; then
|
||||
echo "warning: Node.js version mismatch, consider using nvm to switch to the required version."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "checking .npmrc configuration..."
|
||||
if [ ! -f ".npmrc" ]; then
|
||||
echo "Error: .npmrc file not found, security configurations are missing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checking yarn.lock..."
|
||||
if [ ! -f "yarn.lock" ]; then
|
||||
echo "error: yarn.lock not found, run 'yarn install' to generate it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "running yarn audit..."
|
||||
yarn audit --level moderate
|
||||
|
||||
echo "checking for outdated packages..."
|
||||
yarn outdated || true
|
||||
|
||||
echo "verifying package integrity..."
|
||||
yarn list --depth=0
|
||||
|
||||
echo "checking for known vulnerable packages..."
|
||||
yarn audit --level high
|
||||
|
||||
echo "checking package sources..."
|
||||
yarn list --depth=0 --json | jq -r '.data.trees[] | select(.children) | .children[] | select(.name | test("^https?://(?!registry\\.npmjs\\.org)")) | .name' || true
|
||||
|
||||
echo "checks completed successfully!"
|
||||
echo ""
|
||||
echo "always use 'yarn install --frozen-lockfile' in production environments"
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { createNymMixnetClient, IWebWorkerEvents, MimeTypes, NymClientConfig, NymMixnetClient } from '@nymproject/sdk';
|
||||
|
||||
export interface BinaryMessageHeaders {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("app") as HTMLElement);
|
||||
const root = ReactDOM.createRoot(document.getElementById('app') as HTMLElement);
|
||||
root.render(<App />);
|
||||
|
||||
@@ -47,16 +47,16 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@storybook/addon-actions": "^6.5.8",
|
||||
"@storybook/addon-essentials": "^6.5.8",
|
||||
"@storybook/addon-interactions": "^6.5.8",
|
||||
"@storybook/addon-links": "^6.5.8",
|
||||
"@storybook/builder-webpack5": "^6.5.8",
|
||||
"@storybook/manager-webpack5": "^6.5.8",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@storybook/testing-library": "^0.0.9",
|
||||
"@storybook/addon-actions": "9.0.8",
|
||||
"@storybook/addon-essentials": "^8.4.7",
|
||||
"@storybook/addon-interactions": "^8.4.7",
|
||||
"@storybook/addon-links": "^8.4.7",
|
||||
"@storybook/react": "^8.4.7",
|
||||
"@storybook/react-webpack5": "^8.4.7",
|
||||
"@storybook/testing-library": "0.2.1",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@types/flat": "^5.0.2",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
@@ -80,7 +80,9 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^27.0.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2"
|
||||
"typescript": "^4.6.2",
|
||||
"@nymproject/types": "1.0.0",
|
||||
"@nymproject/mui-theme": "1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
// no-op
|
||||
import '@nymproject/mui-theme';
|
||||
|
||||
export { CurrencyFormField } from './components/currency/CurrencyFormField';
|
||||
export { IdentityKeyFormField } from './components/mixnodes/IdentityKeyFormField';
|
||||
|
||||
@@ -25,14 +25,13 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@storybook/addon-actions": "^6.5.8",
|
||||
"@storybook/addon-essentials": "^6.5.8",
|
||||
"@storybook/addon-interactions": "^6.5.8",
|
||||
"@storybook/addon-links": "^6.5.8",
|
||||
"@storybook/builder-webpack5": "^6.5.8",
|
||||
"@storybook/manager-webpack5": "^6.5.8",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@storybook/testing-library": "^0.0.9",
|
||||
"@storybook/addon-actions": "^8.4.7",
|
||||
"@storybook/addon-essentials": "^8.4.7",
|
||||
"@storybook/addon-interactions": "^8.4.7",
|
||||
"@storybook/addon-links": "^8.4.7",
|
||||
"@storybook/react": "^8.4.7",
|
||||
"@storybook/react-webpack5": "^8.4.7",
|
||||
"@storybook/testing-library": "^0.2.1",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@types/flat": "^5.0.2",
|
||||
"@types/react": "^18.0.26",
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"ts-jest": "^27.0.5",
|
||||
"jest": "^27.1.0",
|
||||
"babel-plugin-root-import": "^5.1.0",
|
||||
"rimraf": "^3.0.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"@types/minimatch": "^5.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
|
||||
Reference in New Issue
Block a user