Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1661f6546d | |||
| 63b78b24cc | |||
| 8926ade582 | |||
| 8a367ea4cf | |||
| 1eb39d1383 | |||
| 60ce7a4d58 | |||
| d0b530e52d | |||
| 95476eee0c | |||
| 08388c0792 | |||
| 884d216728 | |||
| 9533c52a30 | |||
| 7f1a320ef7 | |||
| 614933f7eb | |||
| 05c6768474 | |||
| d80dcd1876 | |||
| 3f0624c0c7 | |||
| 0db8f1f839 | |||
| 202ca37745 | |||
| 6268544c5b | |||
| b0de91b34e | |||
| 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 |
@@ -0,0 +1,57 @@
|
|||||||
|
import { type Environment } from "../src/providers/EnvironmentProvider";
|
||||||
|
|
||||||
|
interface EnvConfig {
|
||||||
|
envName: Environment;
|
||||||
|
basePath: string;
|
||||||
|
apiUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(message?: any, ...optionalParams: any[]) {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === "development" ||
|
||||||
|
process.env.DEBUG_CONFIG_LOGS === "true"
|
||||||
|
) {
|
||||||
|
console.log(message, ...optionalParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMainnetEnv(): EnvConfig {
|
||||||
|
return {
|
||||||
|
envName: "mainnet",
|
||||||
|
basePath: "/explorer",
|
||||||
|
// apiUrl:
|
||||||
|
// process.env.NEXT_PUBLIC_MAINNET_API_URL || "https://nym.com/explorer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSandboxEnv(): EnvConfig {
|
||||||
|
return {
|
||||||
|
envName: "sandbox",
|
||||||
|
basePath: "/sandbox-explorer",
|
||||||
|
// apiUrl:
|
||||||
|
// process.env.NEXT_PUBLIC_SANDBOX_API_URL ||
|
||||||
|
// "https://nym.com/sandbox-explorer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEnvByName = (name: Environment): EnvConfig => {
|
||||||
|
if (name === "sandbox") {
|
||||||
|
return getSandboxEnv();
|
||||||
|
}
|
||||||
|
if (name === "mainnet") {
|
||||||
|
return getMainnetEnv();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to mainnet
|
||||||
|
log("🐼 using mainnet env vars");
|
||||||
|
return getMainnetEnv();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getBasePathByEnv = (env: Environment): string => {
|
||||||
|
return getEnvByName(env).basePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const getApiUrlByEnv = (env: Environment): string => {
|
||||||
|
// return getEnvByName(env).apiUrl;
|
||||||
|
// };
|
||||||
@@ -2,19 +2,29 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
|
||||||
basePath: "/explorer",
|
|
||||||
assetPrefix: "/explorer",
|
|
||||||
trailingSlash: false,
|
trailingSlash: false,
|
||||||
|
|
||||||
async redirects() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
// Change the basePath to /explorer
|
// Rewrite /sandbox-explorer to root
|
||||||
{
|
{
|
||||||
source: "/",
|
source: "/sandbox-explorer",
|
||||||
destination: "/explorer",
|
destination: "/",
|
||||||
basePath: false,
|
},
|
||||||
permanent: true,
|
// Rewrite /explorer to root
|
||||||
|
{
|
||||||
|
source: "/explorer",
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
// Rewrite /sandbox-explorer/* to /*
|
||||||
|
{
|
||||||
|
source: "/sandbox-explorer/:path*",
|
||||||
|
destination: "/:path*",
|
||||||
|
},
|
||||||
|
// Rewrite /explorer/* to /*
|
||||||
|
{
|
||||||
|
source: "/explorer/:path*",
|
||||||
|
destination: "/:path*",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"chain-registry": "^1.69.64",
|
"chain-registry": "^1.69.64",
|
||||||
"cldr-compact-number": "^0.4.0",
|
"cldr-compact-number": "^0.4.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
@@ -49,6 +50,8 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-random-avatars": "^1.3.1",
|
"react-random-avatars": "^1.3.1",
|
||||||
|
"react-simple-maps": "^3.0.0",
|
||||||
|
"react-tooltip": "^5.28.1",
|
||||||
"react-world-flags": "^1.6.0",
|
"react-world-flags": "^1.6.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
@@ -57,6 +60,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/react-simple-maps": "^3.0.6",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.0.3",
|
"eslint-config-next": "15.0.3",
|
||||||
"lefthook": "^1.8.5",
|
"lefthook": "^1.8.5",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../../../../envs/config";
|
||||||
|
|
||||||
|
interface AccountNotFoundClientProps {
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccountNotFoundClient({
|
||||||
|
address,
|
||||||
|
}: AccountNotFoundClientProps) {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExplorerButtonGroup
|
||||||
|
onPage="Account"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Nym Node",
|
||||||
|
isSelected: true,
|
||||||
|
link: `${basePath}/account/${address}/not-found/`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Account",
|
||||||
|
isSelected: false,
|
||||||
|
link: `${basePath}/account/${address}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,62 +1,33 @@
|
|||||||
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
|
||||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||||
import SectionHeading from "@/components/headings/SectionHeading";
|
import SectionHeading from "@/components/headings/SectionHeading";
|
||||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
import Grid from "@mui/material/Grid2";
|
import Grid from "@mui/material/Grid2";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
import AccountNotFoundClient from "./AccountNotFoundClient";
|
||||||
|
|
||||||
export default async function Account({
|
export default async function AccountNotFound({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ address: string }>;
|
params: Promise<{ address: string }>;
|
||||||
}) {
|
}) {
|
||||||
try {
|
const { address } = await params;
|
||||||
const address = (await params).address;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentLayout>
|
<ContentLayout>
|
||||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<SectionHeading title="Nym Node Details" />
|
<SectionHeading title="Nym Node Details" />
|
||||||
<ExplorerButtonGroup
|
<AccountNotFoundClient address={address} />
|
||||||
onPage="Account"
|
</Box>
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: "Nym Node",
|
|
||||||
isSelected: true,
|
|
||||||
link: `/account/${address}/not-found/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Account",
|
|
||||||
isSelected: false,
|
|
||||||
link: `/account/${address}`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Typography variant="h5">
|
</Grid>
|
||||||
<Markdown className="reactMarkDownLink">
|
<Typography variant="h5">
|
||||||
This account does’t have a Nym node bonded. Is this your account?
|
<Markdown className="reactMarkDownLink">
|
||||||
Start [setting up your node](https://nym.com/docs) today!
|
This account doesn't have a Nym node bonded. Is this your
|
||||||
</Markdown>
|
account? Start [setting up your node](https://nym.com/docs) today!
|
||||||
</Typography>
|
</Markdown>
|
||||||
{/* <Grid container columnSpacing={5} rowSpacing={5}>
|
</Typography>
|
||||||
<Grid size={12}>
|
</ContentLayout>
|
||||||
<SectionHeading title="Onboarding" />
|
);
|
||||||
</Grid>
|
|
||||||
<BlogArticlesCards ids={[1]} />
|
|
||||||
</Grid> */}
|
|
||||||
</ContentLayout>
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
let errorMessage = "An error occurred";
|
|
||||||
if (error instanceof Error) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ export default function ExplorerPage() {
|
|||||||
<ContentLayout>
|
<ContentLayout>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Stack gap={5}>
|
<Stack gap={5}>
|
||||||
<SectionHeading title="Explorer" />
|
<SectionHeading title="Servers" />
|
||||||
<NodeAndAddressSearch />
|
<NodeAndAddressSearch />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box sx={{ mt: 5 }}>
|
<Box sx={{ mt: 5 }}>
|
||||||
@@ -13,12 +13,6 @@ export default async function StakingPage() {
|
|||||||
<SubHeaderRow />
|
<SubHeaderRow />
|
||||||
<OverviewCards />
|
<OverviewCards />
|
||||||
<StakeTableWithAction />
|
<StakeTableWithAction />
|
||||||
{/* <Grid container columnSpacing={5} rowSpacing={5}>
|
|
||||||
<Grid size={12}>
|
|
||||||
<SectionHeading title="Onboarding" />
|
|
||||||
</Grid>
|
|
||||||
<BlogArticlesCards ids={[1]} />
|
|
||||||
</Grid> */}
|
|
||||||
</ContentLayout>
|
</ContentLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
|
import { countryCodeMap } from "@/assets/countryCodes";
|
||||||
import { addSeconds } from "date-fns";
|
import { addSeconds } from "date-fns";
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import type {
|
import type {
|
||||||
|
CountryDataResponse,
|
||||||
CurrentEpochData,
|
CurrentEpochData,
|
||||||
|
Environment,
|
||||||
ExplorerData,
|
ExplorerData,
|
||||||
GatewayStatus,
|
GatewayStatus,
|
||||||
IAccountBalancesInfo,
|
IAccountBalancesInfo,
|
||||||
IObservatoryNode,
|
|
||||||
IPacketsAndStakingData,
|
IPacketsAndStakingData,
|
||||||
|
IRewardDetails,
|
||||||
|
NS_NODE,
|
||||||
NodeRewardDetails,
|
NodeRewardDetails,
|
||||||
NymTokenomics,
|
NymTokenomics,
|
||||||
ObservatoryBalance,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
CURRENT_EPOCH,
|
CURRENT_EPOCH,
|
||||||
CURRENT_EPOCH_REWARDS,
|
CURRENT_EPOCH_REWARDS,
|
||||||
DATA_OBSERVATORY_BALANCES_URL,
|
NS_API_MIXNODES_STATS,
|
||||||
DATA_OBSERVATORY_NODES_URL,
|
NS_API_NODES,
|
||||||
NYM_ACCOUNT_ADDRESS,
|
NYM_ACCOUNT_ADDRESS,
|
||||||
NYM_PRICES_API,
|
NYM_PRICES_API,
|
||||||
OBSERVATORY_GATEWAYS_URL,
|
NS_GATEWAYS_URL,
|
||||||
|
SANDBOX_CURRENT_EPOCH,
|
||||||
|
SANDBOX_CURRENT_EPOCH_REWARDS,
|
||||||
|
SANDBOX_NS_API_MIXNODES_STATS,
|
||||||
|
SANDBOX_NS_API_NODES,
|
||||||
|
SANDBOX_NYM_ACCOUNT_ADDRESS,
|
||||||
|
SANDBOX_NS_GATEWAYS_URL,
|
||||||
} from "./urls";
|
} from "./urls";
|
||||||
|
import { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||||
|
|
||||||
// Fetch function for epoch rewards
|
// Fetch function for epoch rewards
|
||||||
export const fetchEpochRewards = async (): Promise<
|
export const fetchEpochRewards = async (
|
||||||
ExplorerData["currentEpochRewardsData"]
|
environment: Environment
|
||||||
> => {
|
): Promise<ExplorerData["currentEpochRewardsData"]> => {
|
||||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_CURRENT_EPOCH_REWARDS
|
||||||
|
: CURRENT_EPOCH_REWARDS;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("CURRENT_EPOCH_REWARDS URL is not defined");
|
||||||
|
}
|
||||||
|
const response = await fetch(baseUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -43,8 +61,12 @@ export const fetchEpochRewards = async (): Promise<
|
|||||||
// Fetch gateway status based on identity key
|
// Fetch gateway status based on identity key
|
||||||
export const fetchGatewayStatus = async (
|
export const fetchGatewayStatus = async (
|
||||||
identityKey: string,
|
identityKey: string,
|
||||||
|
environment: Environment
|
||||||
): Promise<GatewayStatus | null> => {
|
): Promise<GatewayStatus | null> => {
|
||||||
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
|
const baseUrl =
|
||||||
|
environment === "sandbox" ? SANDBOX_NS_GATEWAYS_URL : NS_GATEWAYS_URL;
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/${identityKey}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch gateway status");
|
throw new Error("Failed to fetch gateway status");
|
||||||
@@ -54,17 +76,21 @@ export const fetchGatewayStatus = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchNodeDelegations = async (
|
export const fetchNodeDelegations = async (
|
||||||
id: number,
|
environment: Environment,
|
||||||
|
id: number
|
||||||
): Promise<NodeRewardDetails[]> => {
|
): Promise<NodeRewardDetails[]> => {
|
||||||
const response = await fetch(
|
const baseUrl =
|
||||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
environment === "sandbox" ? SANDBOX_NS_API_NODES : NS_API_NODES;
|
||||||
{
|
|
||||||
headers: {
|
if (!baseUrl) {
|
||||||
Accept: "application/json",
|
throw new Error("NS_API_NODES URL is not defined");
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
}
|
||||||
},
|
const response = await fetch(`${baseUrl}/${id}/delegations`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch delegations");
|
throw new Error("Failed to fetch delegations");
|
||||||
@@ -73,8 +99,14 @@ export const fetchNodeDelegations = async (
|
|||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchCurrentEpoch = async () => {
|
export const fetchCurrentEpoch = async (environment: Environment) => {
|
||||||
const response = await fetch(CURRENT_EPOCH, {
|
const baseUrl =
|
||||||
|
environment === "sandbox" ? SANDBOX_CURRENT_EPOCH : CURRENT_EPOCH;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("NS_API_NODES URL is not defined");
|
||||||
|
}
|
||||||
|
const response = await fetch(baseUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -89,15 +121,22 @@ export const fetchCurrentEpoch = async () => {
|
|||||||
const data: CurrentEpochData = await response.json();
|
const data: CurrentEpochData = await response.json();
|
||||||
const epochEndTime = addSeconds(
|
const epochEndTime = addSeconds(
|
||||||
new Date(data.current_epoch_start),
|
new Date(data.current_epoch_start),
|
||||||
data.epoch_length.secs,
|
data.epoch_length.secs
|
||||||
).toISOString();
|
).toISOString();
|
||||||
|
|
||||||
return { ...data, current_epoch_end: epochEndTime };
|
return { ...data, current_epoch_end: epochEndTime };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch balances based on the address
|
// Fetch balances based on the address
|
||||||
export const fetchBalances = async (address: string): Promise<number> => {
|
export const fetchBalances = async (
|
||||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
address: string,
|
||||||
|
environment: Environment
|
||||||
|
): Promise<number> => {
|
||||||
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NYM_ACCOUNT_ADDRESS
|
||||||
|
: NYM_ACCOUNT_ADDRESS;
|
||||||
|
const response = await fetch(`${baseUrl}/${address}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -108,20 +147,26 @@ export const fetchBalances = async (address: string): Promise<number> => {
|
|||||||
throw new Error("Failed to fetch balances");
|
throw new Error("Failed to fetch balances");
|
||||||
}
|
}
|
||||||
|
|
||||||
const balances: ObservatoryBalance = await response.json();
|
const balances: IAccountBalancesInfo = await response.json();
|
||||||
|
|
||||||
// Calculate total stake
|
// Calculate total stake
|
||||||
return (
|
return (
|
||||||
Number(balances.rewards.staking_rewards.amount) +
|
Number(balances.claimable_rewards.amount) +
|
||||||
Number(balances.delegated.amount)
|
Number(balances.total_delegations.amount)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch function to get total staker rewards
|
// Fetch function to get total staker rewards
|
||||||
export const fetchTotalStakerRewards = async (
|
export const fetchTotalStakerRewards = async (
|
||||||
address: string,
|
address: string,
|
||||||
|
environment: Environment
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NYM_ACCOUNT_ADDRESS
|
||||||
|
: NYM_ACCOUNT_ADDRESS;
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/${address}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -132,15 +177,22 @@ export const fetchTotalStakerRewards = async (
|
|||||||
throw new Error("Failed to fetch balances");
|
throw new Error("Failed to fetch balances");
|
||||||
}
|
}
|
||||||
|
|
||||||
const balances: ObservatoryBalance = await response.json();
|
const balances: IAccountBalancesInfo = await response.json();
|
||||||
|
|
||||||
// Return the staking rewards amount
|
// Return the staking rewards amount
|
||||||
return Number(balances.rewards.staking_rewards.amount);
|
return Number(balances.claimable_rewards.amount);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch function to get the original stake
|
// Fetch function to get the original stake
|
||||||
export const fetchOriginalStake = async (address: string): Promise<number> => {
|
export const fetchOriginalStake = async (
|
||||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
address: string,
|
||||||
|
environment: Environment
|
||||||
|
): Promise<number> => {
|
||||||
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NYM_ACCOUNT_ADDRESS
|
||||||
|
: NYM_ACCOUNT_ADDRESS;
|
||||||
|
const response = await fetch(`${baseUrl}/${address}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -151,19 +203,50 @@ export const fetchOriginalStake = async (address: string): Promise<number> => {
|
|||||||
throw new Error("Failed to fetch balances");
|
throw new Error("Failed to fetch balances");
|
||||||
}
|
}
|
||||||
|
|
||||||
const balances: ObservatoryBalance = await response.json();
|
const balances: IAccountBalancesInfo = await response.json();
|
||||||
|
|
||||||
// Return the delegated amount
|
// Return the delegated amount
|
||||||
return Number(balances.delegated.amount);
|
return Number(balances.total_delegations.amount);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
export const fetchStakerRewards = async (
|
||||||
if (!process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS) {
|
address: string,
|
||||||
throw new Error(
|
environment: Environment
|
||||||
"NEXT_PUBLIC_NS_API_MIXNODES_STATS environment variable is not defined",
|
): Promise<IRewardDetails[]> => {
|
||||||
);
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NYM_ACCOUNT_ADDRESS
|
||||||
|
: NYM_ACCOUNT_ADDRESS;
|
||||||
|
const response = await fetch(`${baseUrl}/${address}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch balances");
|
||||||
}
|
}
|
||||||
const response = await fetch(process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS, {
|
|
||||||
|
const balances: IAccountBalancesInfo = await response.json();
|
||||||
|
|
||||||
|
// Return the delegated amount
|
||||||
|
return balances.accumulated_rewards;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNoise = async (
|
||||||
|
environment: Environment
|
||||||
|
): Promise<IPacketsAndStakingData[]> => {
|
||||||
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NS_API_MIXNODES_STATS
|
||||||
|
: NS_API_MIXNODES_STATS;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("NS_API_MIXNODES_STATS URL is not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(baseUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -177,8 +260,17 @@ export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
|||||||
// Fetch Account Balance
|
// Fetch Account Balance
|
||||||
export const fetchAccountBalance = async (
|
export const fetchAccountBalance = async (
|
||||||
address: string,
|
address: string,
|
||||||
|
environment: Environment
|
||||||
): Promise<IAccountBalancesInfo> => {
|
): Promise<IAccountBalancesInfo> => {
|
||||||
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
|
const baseUrl =
|
||||||
|
environment === "sandbox"
|
||||||
|
? SANDBOX_NYM_ACCOUNT_ADDRESS
|
||||||
|
: NYM_ACCOUNT_ADDRESS;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("NYM_ACCOUNT_ADDRESS URL is not defined");
|
||||||
|
}
|
||||||
|
const res = await fetch(`${baseUrl}/${address}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
@@ -192,39 +284,6 @@ export const fetchAccountBalance = async (
|
|||||||
return data;
|
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
|
// 🔹 Fetch NYM Price
|
||||||
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||||
const res = await fetch(NYM_PRICES_API, {
|
const res = await fetch(NYM_PRICES_API, {
|
||||||
@@ -239,3 +298,119 @@ export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
|||||||
const data: NymTokenomics = await res.json();
|
const data: NymTokenomics = await res.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchNSApiNodes = async (
|
||||||
|
environment: Environment
|
||||||
|
): Promise<NS_NODE[]> => {
|
||||||
|
const baseUrl =
|
||||||
|
environment === "sandbox" ? SANDBOX_NS_API_NODES : NS_API_NODES;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
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(`${baseUrl}?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 (
|
||||||
|
environment: Environment
|
||||||
|
): Promise<{
|
||||||
|
countries: CountryDataResponse;
|
||||||
|
totalCountries: number;
|
||||||
|
uniqueLocations: number;
|
||||||
|
totalServers: number;
|
||||||
|
}> => {
|
||||||
|
// Fetch all nodes from the NS API
|
||||||
|
const nodes = await fetchNSApiNodes(environment);
|
||||||
|
|
||||||
|
// 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 fetchDelegations = async (
|
||||||
|
address: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
nymClient: any
|
||||||
|
): Promise<Delegation[]> => {
|
||||||
|
const data = await nymClient.getDelegatorDelegations({ delegator: address });
|
||||||
|
return data.delegations;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+100
-199
@@ -1,7 +1,3 @@
|
|||||||
export type API_RESPONSE<T> = {
|
|
||||||
data: T[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Denom = "unym" | "nym";
|
export type Denom = "unym" | "nym";
|
||||||
|
|
||||||
export interface IPacketsAndStakingData {
|
export interface IPacketsAndStakingData {
|
||||||
@@ -72,91 +68,62 @@ export interface ExplorerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NodeDescription = {
|
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: {
|
host_information: {
|
||||||
ip_address: string[];
|
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
ip_address: [string, string];
|
||||||
keys: {
|
keys: {
|
||||||
ed25519: string;
|
ed25519: string;
|
||||||
x25519: string;
|
x25519: string;
|
||||||
x25519_noise: string | null;
|
x25519_noise: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
declared_role: {
|
ip_packet_router: {
|
||||||
mixnode: boolean;
|
address: string;
|
||||||
entry: boolean;
|
|
||||||
exit_nr: boolean;
|
|
||||||
exit_ipr: boolean;
|
|
||||||
};
|
};
|
||||||
auxiliary_details: {
|
last_polled: string;
|
||||||
location: string;
|
mixnet_websockets: {
|
||||||
announce_ports: {
|
ws_port: number;
|
||||||
verloc_port: number | null;
|
wss_port: number;
|
||||||
mix_port: number | null;
|
|
||||||
};
|
|
||||||
accepted_operator_terms_and_conditions: boolean;
|
|
||||||
};
|
|
||||||
build_information: {
|
|
||||||
binary_name: string;
|
|
||||||
build_timestamp: string;
|
|
||||||
build_version: string;
|
|
||||||
commit_sha: string;
|
|
||||||
commit_timestamp: string;
|
|
||||||
commit_branch: string;
|
|
||||||
rustc_version: string;
|
|
||||||
rustc_channel: string;
|
|
||||||
cargo_profile: string;
|
|
||||||
cargo_triple: string;
|
|
||||||
};
|
};
|
||||||
network_requester: {
|
network_requester: {
|
||||||
address: string;
|
address: string;
|
||||||
uses_exit_policy: boolean;
|
uses_exit_policy: boolean;
|
||||||
};
|
};
|
||||||
ip_packet_router: {
|
wireguard: {
|
||||||
address: string;
|
port: number;
|
||||||
};
|
public_key: string;
|
||||||
authenticator: {
|
|
||||||
address: string;
|
|
||||||
};
|
|
||||||
wireguard: string | null;
|
|
||||||
mixnet_websockets: {
|
|
||||||
ws_port: number;
|
|
||||||
wss_port: number | null;
|
|
||||||
};
|
};
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type BondInformation = {
|
|
||||||
node_id: number;
|
|
||||||
owner: string;
|
|
||||||
original_pledge: {
|
|
||||||
denom: string;
|
|
||||||
amount: string;
|
|
||||||
};
|
|
||||||
bonding_height: number;
|
|
||||||
is_unbonding: boolean;
|
|
||||||
node: {
|
|
||||||
host: string;
|
|
||||||
custom_http_port: number;
|
|
||||||
identity_key: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RewardingDetails = {
|
|
||||||
cost_params: {
|
|
||||||
profit_margin_percent: string;
|
|
||||||
interval_operating_cost: {
|
|
||||||
denom: string;
|
|
||||||
amount: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
operator: string;
|
|
||||||
delegates: string;
|
|
||||||
total_unit_reward: string;
|
|
||||||
unit_delegation: string;
|
|
||||||
last_rewarded_epoch: number;
|
|
||||||
unique_delegations: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Location = {
|
export type Location = {
|
||||||
two_letter_iso_country_code?: string;
|
two_letter_iso_country_code?: string;
|
||||||
three_letter_iso_country_code?: string;
|
three_letter_iso_country_code?: string;
|
||||||
@@ -165,15 +132,6 @@ export type Location = {
|
|||||||
longitude?: number;
|
longitude?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeData = {
|
|
||||||
node_id: number;
|
|
||||||
contract_node_type: string;
|
|
||||||
description: NodeDescription;
|
|
||||||
bond_information: BondInformation;
|
|
||||||
rewarding_details: RewardingDetails;
|
|
||||||
location: Location;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ACCOUNT BALANCES
|
// ACCOUNT BALANCES
|
||||||
|
|
||||||
export interface IRewardDetails {
|
export interface IRewardDetails {
|
||||||
@@ -193,12 +151,13 @@ export interface IDelegationDetails {
|
|||||||
delegated: IAmountDetails;
|
delegated: IAmountDetails;
|
||||||
height: number;
|
height: number;
|
||||||
proxy: null | string;
|
proxy: null | string;
|
||||||
|
node_bonded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountBalancesInfo {
|
export interface IAccountBalancesInfo {
|
||||||
accumulated_rewards: IRewardDetails[];
|
accumulated_rewards: IRewardDetails[];
|
||||||
address: string;
|
address: string;
|
||||||
balances: IAmountDetails[];
|
balance: IAmountDetails;
|
||||||
claimable_rewards: IAmountDetails;
|
claimable_rewards: IAmountDetails;
|
||||||
delegations: IDelegationDetails[];
|
delegations: IDelegationDetails[];
|
||||||
operator_rewards?: null | IAmountDetails;
|
operator_rewards?: null | IAmountDetails;
|
||||||
@@ -207,111 +166,15 @@ export interface IAccountBalancesInfo {
|
|||||||
vesting_account?: null | string;
|
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 {
|
export interface NodeRewardDetails {
|
||||||
amount: {
|
amount: {
|
||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
};
|
};
|
||||||
|
block_height: number;
|
||||||
cumulative_reward_ratio: string;
|
cumulative_reward_ratio: string;
|
||||||
height: number;
|
|
||||||
node_id: number;
|
|
||||||
owner: string;
|
owner: string;
|
||||||
|
proxy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LastProbeResult = {
|
export type LastProbeResult = {
|
||||||
@@ -425,25 +288,6 @@ export type GatewayStatus = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type BalanceDetails = {
|
|
||||||
amount: number;
|
|
||||||
denom: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ObservatoryRewards = {
|
|
||||||
operator_commissions: BalanceDetails;
|
|
||||||
staking_rewards: BalanceDetails;
|
|
||||||
unlocked: BalanceDetails;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ObservatoryBalance = {
|
|
||||||
delegated: BalanceDetails;
|
|
||||||
locked: BalanceDetails;
|
|
||||||
rewards: ObservatoryRewards;
|
|
||||||
self_bonded: BalanceDetails;
|
|
||||||
spendable: BalanceDetails;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Quote = {
|
export type Quote = {
|
||||||
ath_date: string;
|
ath_date: string;
|
||||||
ath_price: number;
|
ath_price: number;
|
||||||
@@ -480,3 +324,60 @@ export type NymTokenomics = {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
total_supply: number;
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Environment = "mainnet" | "sandbox";
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
export const CURRENT_EPOCH =
|
export const CURRENT_EPOCH =
|
||||||
"https://validator.nymtech.net/api/v1/epoch/current";
|
"https://validator.nymtech.net/api/v1/epoch/current";
|
||||||
|
export const SANDBOX_CURRENT_EPOCH =
|
||||||
|
"https://sandbox-nym-api1.nymtech.net/api/v1/epoch/current";
|
||||||
|
|
||||||
export const CURRENT_EPOCH_REWARDS =
|
export const CURRENT_EPOCH_REWARDS =
|
||||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||||
|
export const SANDBOX_CURRENT_EPOCH_REWARDS =
|
||||||
|
"https://sandbox-nym-api1.nymtech.net/api/v1/epoch/reward_params";
|
||||||
|
|
||||||
export const NYM_ACCOUNT_ADDRESS =
|
export const NYM_ACCOUNT_ADDRESS =
|
||||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
|
"https://validator.nymtech.net/api/v1/unstable/account";
|
||||||
|
export const SANDBOX_NYM_ACCOUNT_ADDRESS =
|
||||||
|
"https://sandbox-nym-api1.nymtech.net/api/v1/unstable/account";
|
||||||
|
|
||||||
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
||||||
|
|
||||||
export const VALIDATOR_BASE_URL =
|
export const VALIDATOR_BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
||||||
export const DATA_OBSERVATORY_NODES_URL =
|
export const SANDBOX_VALIDATOR_BASE_URL = "https://rpc.sandbox.nymtech.net";
|
||||||
"https://api.nym.spectredao.net/api/v1/nodes";
|
|
||||||
|
|
||||||
export const DATA_OBSERVATORY_DELEGATIONS_URL =
|
|
||||||
"https://api.nym.spectredao.net/api/v1/delegations";
|
export const NS_GATEWAYS_URL =
|
||||||
export const DATA_OBSERVATORY_BALANCES_URL =
|
|
||||||
"https://api.nym.spectredao.net/api/v1/balances";
|
|
||||||
export const OBSERVATORY_GATEWAYS_URL =
|
|
||||||
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
||||||
|
export const SANDBOX_NS_GATEWAYS_URL =
|
||||||
|
"https://sandbox-node-status-api.nymte.ch/v2/gateways";
|
||||||
|
|
||||||
export const NS_API_MIXNODES_STATS =
|
export const NS_API_MIXNODES_STATS =
|
||||||
process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS;
|
process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS;
|
||||||
|
export const SANDBOX_NS_API_MIXNODES_STATS =
|
||||||
|
"https://sandbox-node-status-api.nymte.ch/v2/mixnodes/stats";
|
||||||
|
|
||||||
export const NS_API_NODES = process.env.NEXT_PUBLIC_NS_API_NODES;
|
export const NS_API_NODES = process.env.NEXT_PUBLIC_NS_API_NODES;
|
||||||
|
export const SANDBOX_NS_API_NODES =
|
||||||
|
"https://sandbox-node-status-api.nymte.ch/explorer/v3/nym-nodes";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export const TABLET_WIDTH = "(min-width:700px)";
|
export const TABLET_WIDTH = "(min-width:700px)";
|
||||||
|
|
||||||
|
export const RECOMMENDED_NODES = [
|
||||||
|
1362, 291, 1719, 1768, 1772, 1512, 896, 1415, 2114, 2010,
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { WorldMap } from "@/components/worldMap/WorldMap";
|
||||||
import { Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
import Grid from "@mui/material/Grid2";
|
import Grid from "@mui/material/Grid2";
|
||||||
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
|
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
|
||||||
@@ -16,10 +17,11 @@ export default async function Home() {
|
|||||||
<ContentLayout>
|
<ContentLayout>
|
||||||
<Stack gap={5}>
|
<Stack gap={5}>
|
||||||
<NodeAndAddressSearch />
|
<NodeAndAddressSearch />
|
||||||
|
<WorldMap />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<SectionHeading title="Noise Generating Network Overview" />
|
<SectionHeading title="Network Overview" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||||
<NoiseCard />
|
<NoiseCard />
|
||||||
@@ -45,9 +47,9 @@ export default async function Home() {
|
|||||||
<TokenomicsCard />
|
<TokenomicsCard />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container>
|
<Grid container rowSpacing={5}>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<SectionHeading title="Nym Nodes" />
|
<SectionHeading title="Nym Servers" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<NodeTable />
|
<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,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import type { IRewardDetails } from "../../app/api/types";
|
import type { IRewardDetails } from "../../app/api/types";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import { AccountBalancesTable } from "./AccountBalancesTable";
|
import { AccountBalancesTable } from "./AccountBalancesTable";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export interface IAccontStatsRowProps {
|
export interface IAccontStatsRowProps {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -45,7 +46,7 @@ const getAllocation = (unyms: number, totalUnyms: number): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calculateStakingRewards = (
|
const calculateStakingRewards = (
|
||||||
accumulatedRewards: IRewardDetails[],
|
accumulatedRewards: IRewardDetails[]
|
||||||
): number => {
|
): number => {
|
||||||
if (accumulatedRewards.length > 0) {
|
if (accumulatedRewards.length > 0) {
|
||||||
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
|
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
|
||||||
@@ -61,14 +62,17 @@ const calculateStakingRewards = (
|
|||||||
|
|
||||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||||
const { address } = props;
|
const { address } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: accountInfo,
|
data: accountInfo,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["accountBalance", address],
|
queryKey: ["accountBalance", address, environment],
|
||||||
queryFn: () => fetchAccountBalance(address),
|
queryFn: () => fetchAccountBalance(address, environment),
|
||||||
enabled: !!address,
|
enabled: !!address,
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
@@ -77,17 +81,18 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data: nymPrice,
|
data: nymPrice,
|
||||||
isLoading: isLoadingPrice,
|
isLoading: isNymPriceLoading,
|
||||||
error: priceError,
|
isError: isNymPriceError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymPrice"],
|
queryKey: ["nymPrice"],
|
||||||
queryFn: fetchNymPrice,
|
queryFn: fetchNymPrice,
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || isLoadingPrice) {
|
if (isLoading || isNymPriceLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Total value">
|
<ExplorerCard label="Total value">
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
@@ -98,10 +103,13 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || priceError || !accountInfo || !nymPrice) {
|
if (isError || isNymPriceError || !accountInfo || !nymPrice) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Total value">
|
<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.
|
Failed to account data.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Skeleton variant="text" height={238} />
|
<Skeleton variant="text" height={238} />
|
||||||
@@ -113,60 +121,57 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
|||||||
|
|
||||||
const totalBalanceUSD = getPriceInUSD(
|
const totalBalanceUSD = getPriceInUSD(
|
||||||
Number(accountInfo.total_value.amount),
|
Number(accountInfo.total_value.amount),
|
||||||
nymPriceData,
|
nymPriceData
|
||||||
);
|
);
|
||||||
const spendableNYM =
|
const spendableNYM = accountInfo.balance
|
||||||
accountInfo.balances.length > 0
|
? getNymsFormated(Number(accountInfo.balance.amount))
|
||||||
? getNymsFormated(Number(accountInfo.balances[0].amount))
|
: 0;
|
||||||
: 0;
|
const spendableUSD = accountInfo.balance
|
||||||
const spendableUSD =
|
? getPriceInUSD(Number(accountInfo.balance.amount), nymPriceData)
|
||||||
accountInfo.balances.length > 0
|
: 0;
|
||||||
? getPriceInUSD(Number(accountInfo.balances[0].amount), nymPriceData)
|
const spendableAllocation = accountInfo.balance
|
||||||
: 0;
|
? getAllocation(
|
||||||
const spendableAllocation =
|
Number(accountInfo.balance.amount),
|
||||||
accountInfo.balances.length > 0
|
Number(accountInfo.total_value.amount)
|
||||||
? getAllocation(
|
)
|
||||||
Number(accountInfo.balances[0].amount),
|
: 0;
|
||||||
Number(accountInfo.total_value.amount),
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const delegationsNYM = getNymsFormated(
|
const delegationsNYM = getNymsFormated(
|
||||||
Number(accountInfo.total_delegations.amount),
|
Number(accountInfo.total_delegations.amount)
|
||||||
);
|
);
|
||||||
const delegationsUSD = getPriceInUSD(
|
const delegationsUSD = getPriceInUSD(
|
||||||
Number(accountInfo.total_delegations.amount),
|
Number(accountInfo.total_delegations.amount),
|
||||||
nymPriceData,
|
nymPriceData
|
||||||
);
|
);
|
||||||
const delegationsAllocation = getAllocation(
|
const delegationsAllocation = getAllocation(
|
||||||
Number(accountInfo.total_delegations.amount),
|
Number(accountInfo.total_delegations.amount),
|
||||||
Number(accountInfo.total_value.amount),
|
Number(accountInfo.total_value.amount)
|
||||||
);
|
);
|
||||||
|
|
||||||
const operatorRewardsAllocation = getAllocation(
|
const operatorRewardsAllocation = getAllocation(
|
||||||
Number(accountInfo.operator_rewards?.amount || 0),
|
Number(accountInfo.operator_rewards?.amount || 0),
|
||||||
Number(accountInfo.total_value.amount),
|
Number(accountInfo.total_value.amount)
|
||||||
);
|
);
|
||||||
|
|
||||||
const operatorRewardsNYM = getNymsFormated(
|
const operatorRewardsNYM = getNymsFormated(
|
||||||
Number(accountInfo.operator_rewards?.amount || 0),
|
Number(accountInfo.operator_rewards?.amount || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
const operatorRewardsUSD = getPriceInUSD(
|
const operatorRewardsUSD = getPriceInUSD(
|
||||||
Number(accountInfo.operator_rewards?.amount || 0),
|
Number(accountInfo.operator_rewards?.amount || 0),
|
||||||
nymPriceData,
|
nymPriceData
|
||||||
);
|
);
|
||||||
|
|
||||||
const claimableNYM = getNymsFormated(
|
const claimableNYM = getNymsFormated(
|
||||||
Number(accountInfo.claimable_rewards.amount),
|
Number(accountInfo.claimable_rewards.amount)
|
||||||
);
|
);
|
||||||
const claimableUSD = getPriceInUSD(
|
const claimableUSD = getPriceInUSD(
|
||||||
Number(accountInfo.claimable_rewards.amount),
|
Number(accountInfo.claimable_rewards.amount),
|
||||||
nymPriceData,
|
nymPriceData
|
||||||
);
|
);
|
||||||
const claimableAllocation = getAllocation(
|
const claimableAllocation = getAllocation(
|
||||||
Number(accountInfo.claimable_rewards.amount),
|
Number(accountInfo.claimable_rewards.amount),
|
||||||
Number(accountInfo.total_value.amount),
|
Number(accountInfo.total_value.amount)
|
||||||
);
|
);
|
||||||
|
|
||||||
const stakingRewards =
|
const stakingRewards =
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { fetchAccountBalance } from "@/app/api";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
import { CardQRCode } from "../qrCode/QrCode";
|
import { CardQRCode } from "../qrCode/QrCode";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
interface IAccountInfoCardProps {
|
interface IAccountInfoCardProps {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -13,10 +14,13 @@ interface IAccountInfoCardProps {
|
|||||||
|
|
||||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||||
const { address } = props;
|
const { address } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["accountBalance", address],
|
queryKey: ["accountBalance", address, environment],
|
||||||
queryFn: () => fetchAccountBalance(address),
|
queryFn: () => fetchAccountBalance(address, environment),
|
||||||
enabled: !!address,
|
enabled: !!address,
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
@@ -38,7 +42,10 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
|||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Total NYM">
|
<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.
|
Failed to account data.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Skeleton variant="text" height={238} />
|
<Skeleton variant="text" height={238} />
|
||||||
@@ -46,8 +53,7 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const balance =
|
const balance = data.balance ? Number(data.total_value.amount) / 1000000 : 0;
|
||||||
data.balances.length > 0 ? Number(data.total_value.amount) / 1000000 : 0;
|
|
||||||
const balanceFormated = `${balance.toFixed(4)} NYM`;
|
const balanceFormated = `${balance.toFixed(4)} NYM`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,47 +1,58 @@
|
|||||||
"use client";
|
"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 ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
address: string;
|
address: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AccountPageButtonGroup({ address }: Props) {
|
export default function AccountPageButtonGroup({ address }: Props) {
|
||||||
const { data: nymNodes, isError } = useQuery({
|
const { environment } = useEnvironment();
|
||||||
queryKey: ["nymNodes"],
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
queryFn: fetchObservatoryNodes,
|
|
||||||
|
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||||
|
queryKey: ["nsApiNodes", environment],
|
||||||
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: 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;
|
if (!nymNode) return null;
|
||||||
|
|
||||||
if (nymNode.bonding_address)
|
if (nymNode.bonding_address)
|
||||||
return (
|
return (
|
||||||
<ExplorerButtonGroup
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
onPage="Account"
|
<ExplorerButtonGroup
|
||||||
options={[
|
onPage="Account"
|
||||||
{
|
options={[
|
||||||
label: "Nym Node",
|
{
|
||||||
isSelected: false,
|
label: "Nym Node",
|
||||||
link: nymNode
|
isSelected: false,
|
||||||
? `/nym-node/${nymNode.node_id}`
|
link: nymNode
|
||||||
: `/account/${address}/not-found`,
|
? `${basePath}/nym-node/${nymNode.node_id}`
|
||||||
},
|
: `${basePath}/account/${address}/not-found`,
|
||||||
{
|
},
|
||||||
label: "Account",
|
{
|
||||||
isSelected: true,
|
label: "Account",
|
||||||
link: `/account/${address}`,
|
isSelected: true,
|
||||||
},
|
link: `${basePath}/account/${address}`,
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const BlogArticlesCards = async ({
|
|||||||
const blogArticle = JSON.parse(fileContent);
|
const blogArticle = JSON.parse(fileContent);
|
||||||
return {
|
return {
|
||||||
...blogArticle,
|
...blogArticle,
|
||||||
link: `/onboarding/${filename.replace(".json", "")}`,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type BlogArticle = {
|
|||||||
image: string;
|
image: string;
|
||||||
iconLight: string;
|
iconLight: string;
|
||||||
iconDark: string;
|
iconDark: string;
|
||||||
|
link: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
blogAuthors: string[];
|
blogAuthors: string[];
|
||||||
date: Date;
|
date: Date;
|
||||||
@@ -23,8 +24,6 @@ type BlogArticle = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlogArticleWithLink = BlogArticle & {
|
export type BlogArticleWithLink = BlogArticle;
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlogArticle;
|
export default BlogArticle;
|
||||||
|
|||||||
@@ -68,7 +68,12 @@ const ExplorerHeroCard = ({
|
|||||||
const iconSrc = isDarkMode ? iconDarkSrc : iconLightSrc;
|
const iconSrc = isDarkMode ? iconDarkSrc : iconLightSrc;
|
||||||
|
|
||||||
return (
|
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}>
|
<Card sx={dynamicCardStyles} elevation={0}>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCopyToClipboard } from "@uidotdev/usehooks";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import CopyFile from "../icons/CopyFile";
|
import CopyFile from "../icons/CopyFile";
|
||||||
import CopyFileDark from "../icons/CopyFileDark";
|
import CopyFileDark from "../icons/CopyFileDark";
|
||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
|
|
||||||
const CLEAR_AFTER_MS = 10_000;
|
const CLEAR_AFTER_MS = 10_000;
|
||||||
|
|
||||||
@@ -34,12 +35,8 @@ const CopyToClipboard = ({
|
|||||||
|
|
||||||
if (hasCopied) {
|
if (hasCopied) {
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography sx={{ color: isDarkMode ? "base.white" : "pine.950" }}>
|
||||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
<CheckIcon fontSize="small" />
|
||||||
variant="h6"
|
|
||||||
color="textSecondary"
|
|
||||||
>
|
|
||||||
Copied
|
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function Footer() {
|
|||||||
const locale = "en";
|
const locale = "en";
|
||||||
const footerData = await getFooter(locale);
|
const footerData = await getFooter(locale);
|
||||||
const legalContent1 =
|
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 legalContent2 = footerData?.attributes?.legalContent2 || false;
|
||||||
const footerLinkBlocks = footerData?.attributes?.linkBlocks || [];
|
const footerLinkBlocks = footerData?.attributes?.linkBlocks || [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
import { Box, Divider } from "@mui/material";
|
"use client";
|
||||||
|
|
||||||
|
import { Box, Divider, Stack, Button } from "@mui/material";
|
||||||
import NymLogo from "../../components/icons/NymLogo";
|
import NymLogo from "../../components/icons/NymLogo";
|
||||||
import { Link } from "../../components/muiLink";
|
import { Link } from "../../components/muiLink";
|
||||||
import { Wrapper } from "../../components/wrapper";
|
import { Wrapper } from "../../components/wrapper";
|
||||||
import ConnectWallet from "../wallet/ConnectWallet";
|
import ConnectWallet from "../wallet/ConnectWallet";
|
||||||
import HeaderItem from "./HeaderItem";
|
|
||||||
import { DarkLightSwitchDesktop } from "./Switch";
|
import { DarkLightSwitchDesktop } from "./Switch";
|
||||||
import MENU_DATA from "./menuItems";
|
import MENU_DATA from "./menuItems";
|
||||||
|
import { EnvironmentSwitcher } from "./EnvironmentSwitcher";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { Circle } from "@mui/icons-material";
|
||||||
|
|
||||||
export const DesktopHeader = () => {
|
export const DesktopHeader = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
const explorerName = environment
|
||||||
|
? `${environment} Explorer`
|
||||||
|
: "Mainnet Explorer";
|
||||||
|
|
||||||
|
// Helper function to determine if a tab is active
|
||||||
|
const isTabActive = (tabTitle: string) => {
|
||||||
|
// Check if the current pathname matches the tab title
|
||||||
|
// For explorerName, check if we're on the base path
|
||||||
|
if (tabTitle === explorerName) {
|
||||||
|
return pathname === basePath || pathname === basePath + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For menu items, check if the pathname includes the menu URL
|
||||||
|
const menuItem = MENU_DATA.find((menu) => menu.title === tabTitle);
|
||||||
|
if (menuItem) {
|
||||||
|
return pathname.includes(menuItem.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -21,12 +51,12 @@ export const DesktopHeader = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "42px",
|
gap: "30px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={"/"}
|
href={"https://nym.com/"}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -41,15 +71,62 @@ export const DesktopHeader = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "start",
|
justifyContent: "center",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
gap: 5,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "start",
|
||||||
|
height: "100%",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Circle
|
||||||
|
sx={{
|
||||||
|
fontSize: 10,
|
||||||
|
opacity: isTabActive(explorerName) ? 1 : 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Link href={basePath} passHref style={{ textDecoration: "none" }}>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{explorerName}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
{MENU_DATA.map((menu) => (
|
{MENU_DATA.map((menu) => (
|
||||||
<HeaderItem key={menu.id} menu={menu} />
|
<Stack direction="row" gap={1} key={menu.id} alignItems="center">
|
||||||
|
<Circle
|
||||||
|
sx={{
|
||||||
|
fontSize: 10,
|
||||||
|
opacity: isTabActive(menu.title) ? 1 : 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`${basePath}${menu.url}`}
|
||||||
|
style={{ textDecoration: "none" }}
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menu.title}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
<EnvironmentSwitcher />
|
||||||
<ConnectWallet size="small" />
|
<ConnectWallet size="small" />
|
||||||
<DarkLightSwitchDesktop />
|
<DarkLightSwitchDesktop />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useEnvironment } from "../../providers/EnvironmentProvider";
|
||||||
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
import { colours } from "@/theme/colours";
|
||||||
|
|
||||||
|
export const EnvironmentSwitcher: React.FC = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { environment, setEnvironment } = useEnvironment();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const switchNetworkText =
|
||||||
|
environment === "mainnet" ? "Switch to Sandbox" : "Switch to Mainnet";
|
||||||
|
|
||||||
|
const getCurrentInternalPath = () => {
|
||||||
|
// Remove the base path from the current pathname to get the internal path
|
||||||
|
return pathname.replace(/^\/(explorer|sandbox-explorer)/, "") || "/";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchEnvironment = () => {
|
||||||
|
const newEnvironment = environment === "mainnet" ? "sandbox" : "mainnet";
|
||||||
|
setEnvironment(newEnvironment);
|
||||||
|
|
||||||
|
// Get the current internal path and build the new path
|
||||||
|
const currentInternalPath = getCurrentInternalPath();
|
||||||
|
const newBasePath = getBasePathByEnv(newEnvironment);
|
||||||
|
const newPath =
|
||||||
|
currentInternalPath === "/"
|
||||||
|
? newBasePath
|
||||||
|
: `${newBasePath}${currentInternalPath}`;
|
||||||
|
router.push(newPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={handleSwitchEnvironment}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
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,
|
||||||
|
borderStyle: environment === "sandbox" ? "solid" : "dashed",
|
||||||
|
backgroundColor:
|
||||||
|
environment === "sandbox" && theme.palette.mode === "dark"
|
||||||
|
? colours.pine[800]
|
||||||
|
: environment === "sandbox" && theme.palette.mode === "light"
|
||||||
|
? colours.pine[300]
|
||||||
|
: "transparent",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{switchNetworkText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,6 +4,8 @@ import { Circle } from "@mui/icons-material";
|
|||||||
import { Button, Stack } from "@mui/material";
|
import { Button, Stack } from "@mui/material";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useEnvironment } from "../../providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
import type { MenuItem } from "./menuItems";
|
import type { MenuItem } from "./menuItems";
|
||||||
|
|
||||||
type HeaderItemProps = {
|
type HeaderItemProps = {
|
||||||
@@ -12,10 +14,19 @@ type HeaderItemProps = {
|
|||||||
|
|
||||||
const HeaderItem = ({ menu }: HeaderItemProps) => {
|
const HeaderItem = ({ menu }: HeaderItemProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" gap={2} key={menu.id} alignItems="center">
|
<Stack direction="row" gap={2} key={menu.id} alignItems="center">
|
||||||
{pathname.includes(menu.url) && <Circle sx={{ fontSize: 10 }} />}
|
<Circle
|
||||||
<Link href={menu.url} passHref>
|
sx={{
|
||||||
|
fontSize: 10,
|
||||||
|
display: pathname.includes(menu.url) ? "block" : "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link href={`${basePath}${menu.url}`} passHref>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ import NymLogo from "../icons/NymLogo";
|
|||||||
import ConnectWallet from "../wallet/ConnectWallet";
|
import ConnectWallet from "../wallet/ConnectWallet";
|
||||||
import { DarkLightSwitchDesktop } from "./Switch";
|
import { DarkLightSwitchDesktop } from "./Switch";
|
||||||
import MENU_DATA from "./menuItems";
|
import MENU_DATA from "./menuItems";
|
||||||
|
import { EnvironmentSwitcher } from "./EnvironmentSwitcher";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
|
||||||
export const MobileHeader = () => {
|
export const MobileHeader = () => {
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
|
const explorerName = environment
|
||||||
|
? `${environment} Explorer`
|
||||||
|
: "Mainnet Explorer";
|
||||||
|
|
||||||
// Mobile menu handlers
|
// Mobile menu handlers
|
||||||
const toggleDrawer = (open: boolean) => {
|
const toggleDrawer = (open: boolean) => {
|
||||||
@@ -62,6 +71,55 @@ export const MobileHeader = () => {
|
|||||||
>
|
>
|
||||||
{/* Main Menu */}
|
{/* Main Menu */}
|
||||||
<Box sx={{ width: "50%", height: "100%" }}>
|
<Box sx={{ width: "50%", height: "100%" }}>
|
||||||
|
<Box key={explorerName} sx={{ marginBottom: 3 }}>
|
||||||
|
<Link
|
||||||
|
onClick={() => toggleDrawer(false)}
|
||||||
|
href={basePath}
|
||||||
|
target="_self"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
padding: 3.75,
|
||||||
|
color:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "base.white"
|
||||||
|
: "background.main",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
textDecoration: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: 1.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "block",
|
||||||
|
width: "10px",
|
||||||
|
height: "10px",
|
||||||
|
borderRadius: "100%",
|
||||||
|
bgcolor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "base.white"
|
||||||
|
: "primary.main",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
color={
|
||||||
|
theme.palette.mode === "dark" ? "base.white" : "primary"
|
||||||
|
}
|
||||||
|
variant="h4"
|
||||||
|
>
|
||||||
|
{explorerName}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
{MENU_DATA.map((menu) => (
|
{MENU_DATA.map((menu) => (
|
||||||
<Box key={menu.title} sx={{ marginBottom: 3 }}>
|
<Box key={menu.title} sx={{ marginBottom: 3 }}>
|
||||||
<Link
|
<Link
|
||||||
@@ -78,6 +136,7 @@ export const MobileHeader = () => {
|
|||||||
: "background.main",
|
: "background.main",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
textDecoration: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -145,7 +204,7 @@ const MobileMenuHeader = ({
|
|||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
onClick={() => toggleDrawer(false)}
|
onClick={() => toggleDrawer(false)}
|
||||||
href={"/"}
|
href={"https://nym.com/"}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -172,12 +231,13 @@ const MobileMenuHeader = ({
|
|||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 2.5,
|
gap: 4,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!drawerOpen && <EnvironmentSwitcher />}
|
||||||
{!drawerOpen && <ConnectWallet size="small" />}
|
{!drawerOpen && <ConnectWallet size="small" />}
|
||||||
</Box>
|
</Box>
|
||||||
<Box height={40} />
|
<Box height={40} />
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export type MenuItem = {
|
|||||||
const MENU_DATA: MenuItem[] = [
|
const MENU_DATA: MenuItem[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Explorer",
|
title: "Servers",
|
||||||
url: "/table",
|
url: "/servers",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type EpochResponseData,
|
type EpochResponseData,
|
||||||
useEpochContext,
|
useEpochContext,
|
||||||
} from "@/providers/EpochProvider";
|
} from "@/providers/EpochProvider";
|
||||||
import { Skeleton, Typography } from "@mui/material";
|
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||||
import { differenceInMinutes, format } from "date-fns";
|
import { differenceInMinutes, format } from "date-fns";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
@@ -34,10 +34,13 @@ export const CurrentEpochCard = () => {
|
|||||||
const [endTime, setEndTime] = useState("");
|
const [endTime, setEndTime] = useState("");
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
const updateState = useCallback((data: NonNullable<EpochResponseData>) => {
|
const updateState = useCallback((data: NonNullable<EpochResponseData>) => {
|
||||||
const { startTime, endTime } = getStartEndTime(
|
const { startTime, endTime } = getStartEndTime(
|
||||||
data.current_epoch_start,
|
data.current_epoch_start,
|
||||||
data.current_epoch_end,
|
data.current_epoch_end
|
||||||
);
|
);
|
||||||
const progress = calulateProgress(data.current_epoch_end);
|
const progress = calulateProgress(data.current_epoch_end);
|
||||||
|
|
||||||
@@ -65,30 +68,28 @@ export const CurrentEpochCard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError || !data) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Current mixnet epoch">
|
<ExplorerCard label="Current mixnet epoch">
|
||||||
<Typography variant="body3" fontWeight="light">
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||||
|
>
|
||||||
Failed to load data
|
Failed to load data
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<ExplorerCard label="Current mixnet epoch">
|
|
||||||
<Typography variant="body3" fontWeight="light">
|
|
||||||
No data available
|
|
||||||
</Typography>
|
|
||||||
</ExplorerCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (epochStatus === "pending") {
|
if (epochStatus === "pending") {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Current mixnet epoch">
|
<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...
|
Waiting for next epoch to start...
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import type { ExplorerData, IPacketsAndStakingData } from "../../app/api/types";
|
|||||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import { LineChart } from "../lineChart";
|
import { LineChart } from "../lineChart";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const NetworkStakeCard = () => {
|
export const NetworkStakeCard = () => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
const {
|
const {
|
||||||
@@ -15,8 +17,8 @@ export const NetworkStakeCard = () => {
|
|||||||
isLoading: isStakingLoading,
|
isLoading: isStakingLoading,
|
||||||
isError: isStakingError,
|
isError: isStakingError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["noise"],
|
queryKey: ["noise", environment],
|
||||||
queryFn: fetchNoise,
|
queryFn: () => fetchNoise(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -67,8 +69,8 @@ export const NetworkStakeCard = () => {
|
|||||||
.map((item: IPacketsAndStakingData) => ({
|
.map((item: IPacketsAndStakingData) => ({
|
||||||
date_utc: item.date_utc,
|
date_utc: item.date_utc,
|
||||||
numericData: item.total_stake / 1000000,
|
numericData: item.total_stake / 1000000,
|
||||||
}))
|
}));
|
||||||
.filter((item) => item.numericData >= 50_000_000);
|
// .filter((item) => item.numericData >= 50_000_000);
|
||||||
|
|
||||||
const stakeLineGraphData = {
|
const stakeLineGraphData = {
|
||||||
color: "#00CA33",
|
color: "#00CA33",
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ import { formatBigNum } from "../../utils/formatBigNumbers";
|
|||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import { LineChart } from "../lineChart";
|
import { LineChart } from "../lineChart";
|
||||||
import { UpDownPriceIndicator } from "../price/UpDownPriceIndicator";
|
import { UpDownPriceIndicator } from "../price/UpDownPriceIndicator";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const NoiseCard = () => {
|
export const NoiseCard = () => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["noise"],
|
queryKey: ["noise", environment],
|
||||||
queryFn: fetchNoise,
|
queryFn: () => fetchNoise(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -110,8 +112,8 @@ export const NoiseCard = () => {
|
|||||||
date_utc: item.date_utc,
|
date_utc: item.date_utc,
|
||||||
numericData: item.total_packets_sent + item.total_packets_received,
|
numericData: item.total_packets_sent + item.total_packets_received,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.filter((item) => item.numericData >= 2_500_000_000);
|
// .filter((item) => item.numericData >= 2_500_000_000);
|
||||||
|
|
||||||
const handleTooltipOpen = () => {
|
const handleTooltipOpen = () => {
|
||||||
setTooltipOpen(true);
|
setTooltipOpen(true);
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { fetchObservatoryNodes } from "@/app/api";
|
import { fetchNSApiNodes } from "@/app/api";
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const StakersNumberCard = () => {
|
export const StakersNumberCard = () => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: () => fetchObservatoryNodes(),
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -22,7 +25,7 @@ export const StakersNumberCard = () => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Number of delegations">
|
<ExplorerCard label="Number of delegations">
|
||||||
<Skeleton variant="text" height={90} />
|
<Skeleton variant="text" height={90} />
|
||||||
@@ -30,11 +33,11 @@ export const StakersNumberCard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !nymNodes) {
|
if (isNSApiNodesError || !nsApiNodes) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Number of delegations">
|
<ExplorerCard label="Number of delegations">
|
||||||
<Typography
|
<Typography
|
||||||
variant="h3"
|
variant="h5"
|
||||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||||
>
|
>
|
||||||
Failed to load node data.
|
Failed to load node data.
|
||||||
@@ -43,13 +46,13 @@ export const StakersNumberCard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActiveStakersNumber = (nodes: IObservatoryNode[]): number => {
|
const getActiveStakersNumber = (nodes: NS_NODE[]): number => {
|
||||||
return nodes.reduce(
|
return nodes.reduce(
|
||||||
(sum, node) => sum + node.rewarding_details.unique_delegations,
|
(sum, node) => sum + (node.rewarding_details?.unique_delegations || 0),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const allStakers = getActiveStakersNumber(nymNodes);
|
const allStakers = getActiveStakersNumber(nsApiNodes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Number of delegations">
|
<ExplorerCard label="Number of delegations">
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import type { ExplorerData, NymTokenomics } from "../../app/api/types";
|
import type { ExplorerData, NymTokenomics } from "../../app/api/types";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
import { TitlePrice } from "../price/TitlePrice";
|
import { TitlePrice } from "../price/TitlePrice";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const TokenomicsCard = () => {
|
export const TokenomicsCard = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const {
|
const {
|
||||||
data: nymPrice,
|
data: nymPrice,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -27,8 +31,8 @@ export const TokenomicsCard = () => {
|
|||||||
isLoading: isEpochLoading,
|
isLoading: isEpochLoading,
|
||||||
isError: isEpochError,
|
isError: isEpochError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["epochRewards"],
|
queryKey: ["epochRewards", environment],
|
||||||
queryFn: fetchEpochRewards,
|
queryFn: () => fetchEpochRewards(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -40,8 +44,8 @@ export const TokenomicsCard = () => {
|
|||||||
isLoading: isStakingLoading,
|
isLoading: isStakingLoading,
|
||||||
isError: isStakingError,
|
isError: isStakingError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["noise"],
|
queryKey: ["noise", environment],
|
||||||
queryFn: fetchNoise,
|
queryFn: () => fetchNoise(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -69,7 +73,10 @@ export const TokenomicsCard = () => {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Tokenomics overview">
|
<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.
|
Failed to load tokenomics overview.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Skeleton variant="text" height={80} />
|
<Skeleton variant="text" height={80} />
|
||||||
@@ -98,7 +105,7 @@ export const TokenomicsCard = () => {
|
|||||||
function calculateTVL(
|
function calculateTVL(
|
||||||
epochRewards: ExplorerData["currentEpochRewardsData"],
|
epochRewards: ExplorerData["currentEpochRewardsData"],
|
||||||
nymPriceData: NymTokenomics,
|
nymPriceData: NymTokenomics,
|
||||||
packetsAndStaking: ExplorerData["packetsAndStakingData"],
|
packetsAndStaking: ExplorerData["packetsAndStakingData"]
|
||||||
): number {
|
): number {
|
||||||
const lastTotalStake =
|
const lastTotalStake =
|
||||||
packetsAndStaking[packetsAndStaking.length - 1]?.total_stake || 0;
|
packetsAndStaking[packetsAndStaking.length - 1]?.total_stake || 0;
|
||||||
@@ -109,7 +116,7 @@ export const TokenomicsCard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const TVL = formatBigNum(
|
const TVL = formatBigNum(
|
||||||
calculateTVL(epochRewardsData, nymPrice, packetsAndStakingData),
|
calculateTVL(epochRewardsData, nymPrice, packetsAndStakingData)
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataRows = [
|
const dataRows = [
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Slider,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
|
||||||
|
import FilterAltIcon from "@mui/icons-material/FilterAlt";
|
||||||
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
|
import PieChartIcon from "@mui/icons-material/PieChart";
|
||||||
|
import PercentIcon from "@mui/icons-material/Percent";
|
||||||
|
import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup";
|
||||||
|
import { RECOMMENDED_NODES } from "@/app/constants";
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
environment: "mainnet" | "sandbox";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdvancedFilters({
|
||||||
|
uptime,
|
||||||
|
setUptime,
|
||||||
|
saturation,
|
||||||
|
setSaturation,
|
||||||
|
profitMargin,
|
||||||
|
setProfitMargin,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
maxSaturation = 100,
|
||||||
|
activeFilter,
|
||||||
|
setActiveFilter,
|
||||||
|
nodeCounts,
|
||||||
|
environment,
|
||||||
|
}: 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterOptions =
|
||||||
|
environment === "mainnet"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: `Recommended servers (${RECOMMENDED_NODES.length})`,
|
||||||
|
isSelected: activeFilter === "recommended",
|
||||||
|
value: "recommended" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `All servers (${nodeCounts.all})`,
|
||||||
|
isSelected: activeFilter === "all",
|
||||||
|
value: "all" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Mixnodes (${nodeCounts.mixnodes})`,
|
||||||
|
isSelected: activeFilter === "mixnodes",
|
||||||
|
value: "mixnodes" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Gateways (${nodeCounts.gateways})`,
|
||||||
|
isSelected: activeFilter === "gateways",
|
||||||
|
value: "gateways" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: `All servers (${nodeCounts.all})`,
|
||||||
|
isSelected: activeFilter === "all",
|
||||||
|
value: "all" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Mixnodes (${nodeCounts.mixnodes})`,
|
||||||
|
isSelected: activeFilter === "mixnodes",
|
||||||
|
value: "mixnodes" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Gateways (${nodeCounts.gateways})`,
|
||||||
|
isSelected: activeFilter === "gateways",
|
||||||
|
value: "gateways" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (environment === "sandbox" && activeFilter === "recommended") {
|
||||||
|
setActiveFilter("all");
|
||||||
|
}
|
||||||
|
}, [environment, activeFilter, setActiveFilter]);
|
||||||
|
|
||||||
|
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={filterOptions}
|
||||||
|
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 && 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||||
@@ -29,6 +30,9 @@ import StakeModal from "../staking/StakeModal";
|
|||||||
import { fee } from "../staking/schemas";
|
import { fee } from "../staking/schemas";
|
||||||
import ConnectWallet from "../wallet/ConnectWallet";
|
import ConnectWallet from "../wallet/ConnectWallet";
|
||||||
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
|
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
|
||||||
|
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
|
||||||
const ColumnHeading = ({
|
const ColumnHeading = ({
|
||||||
children,
|
children,
|
||||||
@@ -82,7 +86,9 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
identityKey: string;
|
identityKey: string;
|
||||||
}>();
|
}>();
|
||||||
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
||||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const { isWalletConnected } = useChain(chain);
|
||||||
|
|
||||||
const handleRefetch = useCallback(async () => {
|
const handleRefetch = useCallback(async () => {
|
||||||
await queryClient.invalidateQueries();
|
await queryClient.invalidateQueries();
|
||||||
@@ -100,7 +106,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
{ nodeId },
|
{ nodeId },
|
||||||
fee,
|
fee,
|
||||||
"Delegation from Nym Explorer V2",
|
"Delegation from Nym Explorer V2",
|
||||||
uNymFunds,
|
uNymFunds
|
||||||
);
|
);
|
||||||
setSelectedNodeForStaking(undefined);
|
setSelectedNodeForStaking(undefined);
|
||||||
setInfoModalProps({
|
setInfoModalProps({
|
||||||
@@ -129,7 +135,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
[nymClient, handleRefetch],
|
[nymClient, handleRefetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSelectStake = useCallback(
|
const handleOnSelectStake = useCallback(
|
||||||
@@ -158,7 +164,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
identityKey: node.identity_key,
|
identityKey: node.identity_key,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[isWalletConnected],
|
[isWalletConnected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
||||||
@@ -189,54 +195,6 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
},
|
},
|
||||||
Cell: ({ row }) => <Favorite address={row.original.owner} />,
|
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",
|
id: "location",
|
||||||
header: "Location",
|
header: "Location",
|
||||||
@@ -255,6 +213,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",
|
id: "stakeSaturation",
|
||||||
header: "Stake saturation",
|
header: "Stake saturation",
|
||||||
@@ -262,9 +312,58 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
size: 120,
|
size: 120,
|
||||||
|
|
||||||
Header: <ColumnHeading>Saturation</ColumnHeading>,
|
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 }) => (
|
Cell: ({ row }) => (
|
||||||
<Typography variant="body4">
|
<Typography variant="body4">
|
||||||
{row.original.stakeSaturation}%
|
{row.original.operatingCosts} NYM
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -303,7 +402,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[isWalletConnected, handleOnSelectStake, favorites],
|
[isWalletConnected, handleOnSelectStake, favorites, isDarkMode]
|
||||||
);
|
);
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
columns,
|
columns,
|
||||||
@@ -410,8 +509,14 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
muiTableBodyRowProps: ({ row }) => ({
|
muiTableBodyRowProps: ({ row }) => ({
|
||||||
onClick: () => {
|
onClick: (e) => {
|
||||||
router.push(`/nym-node/${row.original.nodeId}`);
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
window.open(`${basePath}/nym-node/${row.original.nodeId}`, "_blank");
|
||||||
|
} else {
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
router.push(`${basePath}/nym-node/${row.original.nodeId}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
hover: true,
|
hover: true,
|
||||||
sx: {
|
sx: {
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||||
import { countryName } from "../../utils/countryName";
|
import { countryName } from "../../utils/countryName";
|
||||||
import NodeTable from "./NodeTable";
|
import NodeTable from "./NodeTable";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import AdvancedFilters from "./AdvancedFilters";
|
||||||
|
import { RECOMMENDED_NODES } from "@/app/constants";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
// Utility function to calculate node saturation point
|
// Utility function to calculate node saturation point
|
||||||
function getNodeSaturationPoint(
|
function getNodeSaturationPoint(
|
||||||
totalStake: number,
|
totalStake: number,
|
||||||
stakeSaturationPoint: string,
|
stakeSaturationPoint: string
|
||||||
): number {
|
): number {
|
||||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||||
|
|
||||||
@@ -25,47 +29,139 @@ function getNodeSaturationPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map nodes with rewards data
|
// Map nodes with rewards data
|
||||||
const mappedNymNodes = (
|
|
||||||
nodes: IObservatoryNode[],
|
const mappedNSApiNodes = (
|
||||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
nodes: NS_NODE[],
|
||||||
|
epochRewardsData: ExplorerData["currentEpochRewardsData"]
|
||||||
) =>
|
) =>
|
||||||
nodes.map((node) => {
|
nodes
|
||||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
.map((node) => {
|
||||||
node.total_stake,
|
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||||
epochRewardsData.interval.stake_saturation_point,
|
+node.total_stake,
|
||||||
);
|
epochRewardsData.interval.stake_saturation_point
|
||||||
|
);
|
||||||
|
|
||||||
const cleanMoniker = DOMPurify.sanitize(
|
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||||
node.self_description.moniker,
|
/&/g,
|
||||||
).replace(/&/g, "&");
|
"&"
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
const selfBondFormatted = node.original_pledge
|
||||||
name: cleanMoniker,
|
? Number(node.original_pledge) / 1_000_000
|
||||||
nodeId: node.node_id,
|
: 0;
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Sort alphabetically by country name
|
||||||
|
return a.countryName.localeCompare(b.countryName);
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
|
||||||
export type MappedNymNode = MappedNymNodes[0];
|
export type MappedNymNode = MappedNymNodes[0];
|
||||||
|
|
||||||
const NodeTableWithAction = () => {
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
|
// 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
|
// Use React Query to fetch epoch rewards
|
||||||
const {
|
const {
|
||||||
data: epochRewardsData,
|
data: epochRewardsData,
|
||||||
isLoading: isEpochLoading,
|
isLoading: isEpochLoading,
|
||||||
isError: isEpochError,
|
isError: isEpochError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["epochRewards"],
|
queryKey: ["epochRewards", environment],
|
||||||
queryFn: fetchEpochRewards,
|
queryFn: () => fetchEpochRewards(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -74,20 +170,48 @@ const NodeTableWithAction = () => {
|
|||||||
|
|
||||||
// Use React Query to fetch Nym nodes
|
// Use React Query to fetch Nym nodes
|
||||||
const {
|
const {
|
||||||
data: nymNodes = [],
|
data: nsApiNodes = [],
|
||||||
isLoading: isNodesLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError: isNodesError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// Handle loading state
|
||||||
if (isEpochLoading || isNodesLoading) {
|
if (isEpochLoading || isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<Card sx={{ height: "100%", mt: 5 }}>
|
<Card sx={{ height: "100%", mt: 5 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -101,7 +225,7 @@ const NodeTableWithAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle error state
|
// Handle error state
|
||||||
if (isEpochError || isNodesError) {
|
if (isEpochError || isNSApiNodesError) {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||||
@@ -112,14 +236,68 @@ const NodeTableWithAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map nodes with rewards data
|
// Map nodes with rewards data
|
||||||
|
|
||||||
if (!epochRewardsData) {
|
if (!epochRewardsData) {
|
||||||
return null;
|
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 RECOMMENDED_NODES.includes(node.nodeId);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return <NodeTable nodes={data} />;
|
// Step 2: If advanced filters are open, apply them only if sliders are not at default
|
||||||
|
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;
|
||||||
|
|
||||||
|
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}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
<NodeTable nodes={filteredNodes} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NodeTableWithAction;
|
export default NodeTableWithAction;
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchObservatoryNodes } from "../../app/api";
|
import { fetchNSApiNodes } from "../../app/api";
|
||||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BasicInfoCard = ({ paramId }: Props) => {
|
export const BasicInfoCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
|
if (isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Basic info">
|
<ExplorerCard label="Basic info">
|
||||||
<Skeleton variant="text" height={90} />
|
<Skeleton variant="text" height={90} />
|
||||||
@@ -42,10 +47,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !nymNodes) {
|
if (!nsApiNodes || isNSApiNodesError) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Basic info">
|
<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.
|
Failed to load node data.
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
@@ -55,16 +63,20 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
|||||||
// get node info based on wether it's dentity_key or node_id
|
// get node info based on wether it's dentity_key or node_id
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} 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;
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
const selfBond = formatBigNum(
|
const selfBond = nodeInfo.original_pledge
|
||||||
Number(nodeInfo.rewarding_details.operator) / 1_000_000,
|
? formatBigNum(Number(nodeInfo.original_pledge) / 1_000_000)
|
||||||
);
|
: 0;
|
||||||
const selfBondFormatted = `${selfBond} NYM`;
|
const selfBondFormatted = `${selfBond} NYM`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,9 +97,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
|||||||
variant="body4"
|
variant="body4"
|
||||||
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
|
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
|
||||||
>
|
>
|
||||||
{nodeInfo.bonding_address}
|
{nodeInfo.bonding_address
|
||||||
|
? nodeInfo.bonding_address
|
||||||
|
: "Node not bonded"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
{nodeInfo.bonding_address && (
|
||||||
|
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -113,12 +129,14 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExplorerListItem
|
{nodeInfo.rewarding_details && (
|
||||||
row
|
<ExplorerListItem
|
||||||
divider
|
row
|
||||||
label="Nr. of stakers"
|
divider
|
||||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
label="Nr. of stakers"
|
||||||
/>
|
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ExplorerListItem row label="Self bonded" value={selfBondFormatted} />
|
<ExplorerListItem row label="Self bonded" value={selfBondFormatted} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { NodeRewardDetails } from "../../app/api/types";
|
import type { NodeRewardDetails } from "../../app/api/types";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
|
||||||
const ColumnHeading = ({
|
const ColumnHeading = ({
|
||||||
children,
|
children,
|
||||||
@@ -36,10 +38,12 @@ type Props = {
|
|||||||
const DelegationsTable = ({ id }: Props) => {
|
const DelegationsTable = ({ id }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
const { data: delegations = [], isError } = useQuery({
|
const { data: delegations = [], isError } = useQuery({
|
||||||
queryKey: ["nodeDelegations", id],
|
queryKey: ["nodeDelegations", id, environment],
|
||||||
queryFn: () => fetchNodeDelegations(id),
|
queryFn: () => fetchNodeDelegations(environment, id),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -55,7 +59,7 @@ const DelegationsTable = ({ id }: Props) => {
|
|||||||
accessorKey: "height",
|
accessorKey: "height",
|
||||||
Cell: ({ row }) => (
|
Cell: ({ row }) => (
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Typography variant="body4">{row.original.height}</Typography>
|
<Typography variant="body4">{row.original.block_height}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -86,7 +90,7 @@ const DelegationsTable = ({ id }: Props) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
columns,
|
columns,
|
||||||
@@ -139,7 +143,7 @@ const DelegationsTable = ({ id }: Props) => {
|
|||||||
},
|
},
|
||||||
muiTableBodyRowProps: ({ row }) => ({
|
muiTableBodyRowProps: ({ row }) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push(`/account/${row.original.owner}`);
|
router.push(`${basePath}/account/${row.original.owner}`);
|
||||||
},
|
},
|
||||||
hover: true,
|
hover: true,
|
||||||
sx: {
|
sx: {
|
||||||
|
|||||||
@@ -1,48 +1,40 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import { Skeleton, Typography } from "@mui/material";
|
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
import { fetchNSApiNodes } from "../../app/api";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeDataCard = ({ paramId }: Props) => {
|
export const NodeDataCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const {
|
|
||||||
data: epochRewardsData,
|
|
||||||
isLoading: isEpochLoading,
|
|
||||||
isError: isEpochError,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["epochRewards"],
|
|
||||||
queryFn: fetchEpochRewards,
|
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch node information
|
// Fetch node information
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isEpochLoading || isLoading) {
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
|
if (isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||||
<Skeleton variant="text" height={50} />
|
<Skeleton variant="text" height={50} />
|
||||||
@@ -53,10 +45,13 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
if (isNSApiNodesError || !nsApiNodes) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
<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.
|
Failed to load node data.
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
@@ -66,17 +61,23 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
|||||||
// get node info based on wether it's dentity_key or node_id
|
// get node info based on wether it's dentity_key or node_id
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} 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;
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
const softwareUpdateTime = format(
|
const softwareUpdateTime = nodeInfo.self_description
|
||||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
? format(
|
||||||
"dd/MM/yyyy",
|
new Date(nodeInfo.self_description.build_information.build_timestamp),
|
||||||
);
|
"dd/MM/yyyy"
|
||||||
|
)
|
||||||
|
: "N/A";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||||
@@ -90,13 +91,21 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
|||||||
row
|
row
|
||||||
divider
|
divider
|
||||||
label="Host"
|
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
|
<ExplorerListItem
|
||||||
row
|
row
|
||||||
divider
|
divider
|
||||||
label="Version"
|
label="Version"
|
||||||
value={nodeInfo.description.build_information.build_version}
|
value={
|
||||||
|
nodeInfo.self_description
|
||||||
|
? nodeInfo.self_description.build_information.build_version
|
||||||
|
: "N/A"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ExplorerListItem
|
<ExplorerListItem
|
||||||
row
|
row
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { fetchObservatoryNodes } from "@/app/api";
|
import { fetchNSApiNodes } from "@/app/api";
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import { Skeleton, Typography } from "@mui/material";
|
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import DelegationsTable from "./DelegationsTable";
|
import DelegationsTable from "./DelegationsTable";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeDelegationsCard = ({ paramId }: Props) => {
|
const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isError,
|
isLoading: isNSApiNodesLoading,
|
||||||
isLoading,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -28,16 +32,20 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} 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;
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
const id = nodeInfo.node_id;
|
const id = nodeInfo.node_id;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||||
<Skeleton variant="text" height={50} />
|
<Skeleton variant="text" height={50} />
|
||||||
@@ -48,10 +56,13 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isNSApiNodesError) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
<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.
|
Failed to load delegations. Please try again later.
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
|
|||||||
@@ -1,54 +1,65 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { fetchObservatoryNodes } from "@/app/api";
|
import { fetchNSApiNodes } from "@/app/api";
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NodePageButtonGroup({ paramId }: Props) {
|
export default function NodePageButtonGroup({ paramId }: Props) {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
const { data: nymNodes, isError } = useQuery({
|
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: 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
|
// get node info based on wether it's dentity_key or node_id
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} 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;
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
if (nodeInfo.bonding_address)
|
if (nodeInfo.bonding_address)
|
||||||
return (
|
return (
|
||||||
<ExplorerButtonGroup
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
onPage="Nym Node"
|
<ExplorerButtonGroup
|
||||||
options={[
|
onPage="Node"
|
||||||
{
|
options={[
|
||||||
label: "Nym Node",
|
{
|
||||||
isSelected: true,
|
label: "Nym Node",
|
||||||
link: `/nym-node/${nodeInfo.node_id}`,
|
isSelected: true,
|
||||||
},
|
link: `${basePath}/nym-node/${nodeInfo.identity_key}`,
|
||||||
{
|
},
|
||||||
label: "Account",
|
{
|
||||||
isSelected: false,
|
label: "Account",
|
||||||
link: `/account/${nodeInfo.bonding_address}`,
|
isSelected: false,
|
||||||
},
|
link: `${basePath}/account/${nodeInfo.bonding_address}`,
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||||
import type { IObservatoryNode, RewardingDetails } from "../../app/api/types";
|
import type { NS_NODE } from "../../app/api/types";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeParametersCard = ({ paramId }: Props) => {
|
export const NodeParametersCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
// Fetch epoch rewards
|
// Fetch epoch rewards
|
||||||
const {
|
const {
|
||||||
@@ -21,8 +25,8 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
isLoading: isEpochLoading,
|
isLoading: isEpochLoading,
|
||||||
isError: isEpochError,
|
isError: isEpochError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["epochRewards"],
|
queryKey: ["epochRewards", environment],
|
||||||
queryFn: fetchEpochRewards,
|
queryFn: () => fetchEpochRewards(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -31,19 +35,19 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
// Fetch node information
|
// Fetch node information
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isEpochLoading || isLoading) {
|
if (isEpochLoading || isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
||||||
<Skeleton variant="text" height={50} />
|
<Skeleton variant="text" height={50} />
|
||||||
@@ -54,10 +58,13 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
<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.
|
Failed to load node data.
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
@@ -66,9 +73,13 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
// get node info based on wether it's dentity_key or node_id
|
// get node info based on wether it's dentity_key or node_id
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} 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;
|
if (!nodeInfo) return null;
|
||||||
@@ -76,23 +87,27 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
const totalStake = formatBigNum(Number(nodeInfo.total_stake) / 1_000_000);
|
const totalStake = formatBigNum(Number(nodeInfo.total_stake) / 1_000_000);
|
||||||
const totalStakeFormatted = `${totalStake} NYM`;
|
const totalStakeFormatted = `${totalStake} NYM`;
|
||||||
|
|
||||||
// Extract reward details
|
|
||||||
const rewardDetails: RewardingDetails = nodeInfo.rewarding_details;
|
|
||||||
|
|
||||||
const profitMarginPercent =
|
// Extract reward details
|
||||||
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 profitMarginPercentFormated = `${profitMarginPercent}%`;
|
||||||
|
|
||||||
const operatingCosts =
|
const operatingCosts = nodeInfo.rewarding_details
|
||||||
Number(rewardDetails.cost_params.interval_operating_cost.amount) /
|
? Number(
|
||||||
1_000_000;
|
nodeInfo.rewarding_details.cost_params.interval_operating_cost.amount
|
||||||
|
) / 1_000_000
|
||||||
|
: 0;
|
||||||
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
||||||
|
|
||||||
const getNodeSaturationPoint = (
|
const getNodeSaturationPoint = (
|
||||||
totalStake: number,
|
nodeTotalStake: string,
|
||||||
stakeSaturationPoint: string,
|
stakeSaturationPoint: string
|
||||||
): string => {
|
): string => {
|
||||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||||
|
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||||
|
|
||||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||||
throw new Error("Invalid stake saturation point provided");
|
throw new Error("Invalid stake saturation point provided");
|
||||||
@@ -105,7 +120,7 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||||
nodeInfo.total_stake,
|
nodeInfo.total_stake,
|
||||||
epochRewardsData.interval.stake_saturation_point,
|
epochRewardsData.interval.stake_saturation_point
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { IObservatoryNode } from "@/app/api/types";
|
import type { NS_NODE } from "@/app/api/types";
|
||||||
import { useChain } from "@cosmos-kit/react";
|
import { useChain } from "@cosmos-kit/react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { RandomAvatar } from "react-random-avatars";
|
import { RandomAvatar } from "react-random-avatars";
|
||||||
import { fetchObservatoryNodes } from "../../app/api";
|
import { fetchNSApiNodes } from "../../app/api";
|
||||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||||
import { useNymClient } from "../../hooks/useNymClient";
|
import { useNymClient } from "../../hooks/useNymClient";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
@@ -25,16 +25,19 @@ import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
|||||||
import StakeModal from "../staking/StakeModal";
|
import StakeModal from "../staking/StakeModal";
|
||||||
import { fee } from "../staking/schemas";
|
import { fee } from "../staking/schemas";
|
||||||
import ConnectWallet from "../wallet/ConnectWallet";
|
import ConnectWallet from "../wallet/ConnectWallet";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeProfileCard = ({ paramId }: Props) => {
|
export const NodeProfileCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
let nodeInfo: NS_NODE | undefined;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
|
||||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { isWalletConnected } = useChain(chain);
|
||||||
const { nymClient } = useNymClient();
|
const { nymClient } = useNymClient();
|
||||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -47,12 +50,12 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
// Fetch node info
|
// Fetch node info
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading: isLoadingNymNodes,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -62,9 +65,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
// get node info based on wether it's dentity_key or node_id
|
// get node info based on wether it's dentity_key or node_id
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.identity_key === paramId
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
nodeInfo = nsApiNodes.find(
|
||||||
|
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnSelectStake = useCallback(() => {
|
const handleOnSelectStake = useCallback(() => {
|
||||||
@@ -96,7 +103,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [isWalletConnected, nodeInfo]);
|
}, [isWalletConnected, nodeInfo]);
|
||||||
|
|
||||||
if (isLoadingNymNodes) {
|
if (isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||||
<Skeleton variant="rectangular" height={80} width={80} />
|
<Skeleton variant="rectangular" height={80} width={80} />
|
||||||
@@ -105,11 +112,11 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isError || !nymNodes) {
|
if (isNSApiNodesError || !nsApiNodes) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h3"
|
variant="h5"
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.mode === "dark" ? "base.white" : "pine.950",
|
color: theme.palette.mode === "dark" ? "base.white" : "pine.950",
|
||||||
}}
|
}}
|
||||||
@@ -137,7 +144,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
{ nodeId },
|
{ nodeId },
|
||||||
fee,
|
fee,
|
||||||
"Delegation from Nym Explorer V2",
|
"Delegation from Nym Explorer V2",
|
||||||
uNymFunds,
|
uNymFunds
|
||||||
);
|
);
|
||||||
setSelectedNodeForStaking(undefined);
|
setSelectedNodeForStaking(undefined);
|
||||||
setInfoModalProps({
|
setInfoModalProps({
|
||||||
@@ -164,12 +171,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
if (!nodeInfo) return null;
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
const cleanMoniker = DOMPurify.sanitize(
|
const cleanMoniker = DOMPurify.sanitize(nodeInfo.description.moniker).replace(
|
||||||
nodeInfo?.self_description.moniker,
|
/&/g,
|
||||||
).replace(/&/g, "&");
|
"&"
|
||||||
|
);
|
||||||
|
|
||||||
const cleanDescription = DOMPurify.sanitize(
|
const cleanDescription = DOMPurify.sanitize(
|
||||||
nodeInfo?.self_description.details,
|
nodeInfo.description.details
|
||||||
).replace(/&/g, "&");
|
).replace(/&/g, "&");
|
||||||
|
|
||||||
// get full country name
|
// get full country name
|
||||||
@@ -197,7 +205,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
>
|
>
|
||||||
{cleanMoniker || "Moniker"}
|
{cleanMoniker || "Moniker"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{nodeInfo.description.auxiliary_details.location && (
|
{nodeInfo.geoip?.country && (
|
||||||
<Box display={"flex"} gap={1}>
|
<Box display={"flex"} gap={1}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
@@ -210,10 +218,8 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<CountryFlag
|
<CountryFlag
|
||||||
countryCode={nodeInfo.description.auxiliary_details.location}
|
countryCode={nodeInfo.geoip?.country || ""}
|
||||||
countryName={countryName(
|
countryName={countryName(nodeInfo.geoip?.country || "")}
|
||||||
nodeInfo.description.auxiliary_details.location,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
"use client";
|
"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 { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
fetchEpochRewards,
|
fetchEpochRewards,
|
||||||
fetchGatewayStatus,
|
fetchGatewayStatus,
|
||||||
fetchObservatoryNodes,
|
fetchNSApiNodes,
|
||||||
} from "../../app/api";
|
} from "../../app/api";
|
||||||
import type {
|
import type {
|
||||||
IObservatoryNode,
|
|
||||||
LastProbeResult,
|
LastProbeResult,
|
||||||
NodeDescription,
|
NodeDescription,
|
||||||
|
NS_NODE,
|
||||||
} from "../../app/api/types";
|
} from "../../app/api/types";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
import ExplorerListItem from "../list/ListItem";
|
import ExplorerListItem from "../list/ListItem";
|
||||||
import StarRating from "../starRating/StarRating";
|
import StarRating from "../starRating/StarRating";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
type Props = {
|
type Props = {
|
||||||
paramId: string;
|
paramId: string;
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,7 @@ const roleMapping: Record<DeclaredRoleKey, RoleString> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getNodeRoles = (
|
const getNodeRoles = (
|
||||||
declaredRoles: NodeDescriptionNotNull["declared_role"],
|
declaredRoles: NodeDescriptionNotNull["declared_role"]
|
||||||
): RoleString[] => {
|
): RoleString[] => {
|
||||||
return Object.entries(declaredRoles)
|
return Object.entries(declaredRoles)
|
||||||
.filter(([, isActive]) => isActive)
|
.filter(([, isActive]) => isActive)
|
||||||
@@ -81,7 +81,7 @@ function calculateConfigScoreStars(probeResult: LastProbeResult): number {
|
|||||||
|
|
||||||
if (as_entry) {
|
if (as_entry) {
|
||||||
const entryScore = [as_entry.can_connect, as_entry.can_route].filter(
|
const entryScore = [as_entry.can_connect, as_entry.can_route].filter(
|
||||||
Boolean,
|
Boolean
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1;
|
return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1;
|
||||||
@@ -148,28 +148,31 @@ function calculateWireguardPerformance(probeResult: LastProbeResult): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NodeRoleCard = ({ paramId }: Props) => {
|
export const NodeRoleCard = ({ paramId }: Props) => {
|
||||||
let nodeInfo: IObservatoryNode | undefined;
|
let nodeInfo: NS_NODE | undefined;
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
// Fetch node info
|
// Fetch node info
|
||||||
const {
|
const {
|
||||||
data: nymNodes,
|
data: nsApiNodes = [],
|
||||||
isLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: epochRewardsData,
|
data: epochRewardsData,
|
||||||
isLoading: isEpochLoading,
|
isLoading: isEpochLoading,
|
||||||
isError: isEpochError,
|
isError: isEpochError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["epochRewards"],
|
queryKey: ["epochRewards", environment],
|
||||||
queryFn: fetchEpochRewards,
|
queryFn: () => fetchEpochRewards(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -177,30 +180,31 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (paramId.length > 10) {
|
if (paramId.length > 10) {
|
||||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
nodeInfo = nsApiNodes.find((node) => node.identity_key === paramId);
|
||||||
} else {
|
} 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
|
} // 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
|
// Define whether to fetch gateway status
|
||||||
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
|
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
|
||||||
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
|
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
|
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
|
||||||
const { data: gatewayStatus } = useQuery({
|
const { data: gatewayStatus } = useQuery({
|
||||||
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
|
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
|
||||||
queryFn: () => fetchGatewayStatus(nodeInfo?.identity_key || ""),
|
queryFn: () =>
|
||||||
|
fetchGatewayStatus(nodeInfo?.identity_key || "", environment),
|
||||||
enabled: !!nodeInfo?.identity_key && shouldFetchGatewayStatus, // ✅ Only fetch if needed
|
enabled: !!nodeInfo?.identity_key && shouldFetchGatewayStatus, // ✅ Only fetch if needed
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || isEpochLoading) {
|
if (isNSApiNodesLoading || isEpochLoading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Node role & performance">
|
<ExplorerCard label="Node role & performance">
|
||||||
<Skeleton variant="text" height={70} />
|
<Skeleton variant="text" height={70} />
|
||||||
@@ -210,15 +214,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !nymNodes || !epochRewardsData || isEpochError) {
|
if (isNSApiNodesError || !nsApiNodes || !epochRewardsData || isEpochError) {
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Node role & performance">
|
<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.
|
Failed to load node data.
|
||||||
</Typography>
|
</Typography>
|
||||||
</ExplorerCard>
|
</ExplorerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!nodeInfo) return null;
|
||||||
|
|
||||||
const NodeRoles = nodeRoles.map((role) => (
|
const NodeRoles = nodeRoles.map((role) => (
|
||||||
<Stack key={role} direction="row" gap={1}>
|
<Stack key={role} direction="row" gap={1}>
|
||||||
@@ -226,8 +234,6 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!nodeInfo) return null;
|
|
||||||
|
|
||||||
const qualityOfServiceStars = nodeInfo?.uptime
|
const qualityOfServiceStars = nodeInfo?.uptime
|
||||||
? calculateQualityOfServiceStars(nodeInfo.uptime)
|
? calculateQualityOfServiceStars(nodeInfo.uptime)
|
||||||
: gatewayStatus
|
: gatewayStatus
|
||||||
@@ -247,9 +253,10 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
|||||||
|
|
||||||
// Function to calculate active set probability
|
// Function to calculate active set probability
|
||||||
const getActiveSetProbability = (
|
const getActiveSetProbability = (
|
||||||
totalStake: number,
|
nodeTotalStake: string,
|
||||||
stakeSaturationPoint: string,
|
stakeSaturationPoint: string
|
||||||
): string => {
|
): string => {
|
||||||
|
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||||
|
|
||||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||||
@@ -268,7 +275,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
|||||||
};
|
};
|
||||||
const activeSetProb = getActiveSetProbability(
|
const activeSetProb = getActiveSetProbability(
|
||||||
nodeInfo.total_stake,
|
nodeInfo.total_stake,
|
||||||
epochRewardsData.interval.stake_saturation_point,
|
epochRewardsData.interval.stake_saturation_point
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"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 { Search } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
@@ -12,29 +13,36 @@ import {
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { fetchObservatoryNodes } from "../../app/api";
|
import { fetchNSApiNodes } from "../../app/api";
|
||||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
|
||||||
const NodeAndAddressSearch = () => {
|
const NodeAndAddressSearch = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [searchOptions, setSearchOptions] = useState<IObservatoryNode[]>([]);
|
const [searchOptions, setSearchOptions] = useState<NS_NODE[]>([]);
|
||||||
|
|
||||||
// Use React Query to fetch nodes
|
// Use React Query to fetch nodes
|
||||||
const { data: nymNodes = [], isLoading: isLoadingNodes } = useQuery({
|
const { data: nsApiNodes = [], isLoading: isNSApiNodesLoading } = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
setErrorText(""); // Clear any previous error messages
|
if (!inputValue.trim()) {
|
||||||
setIsLoading(true); // Start loading
|
setErrorText("Please enter a search term");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorText("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (inputValue.startsWith("n1")) {
|
if (inputValue.startsWith("n1")) {
|
||||||
@@ -45,7 +53,8 @@ const NodeAndAddressSearch = () => {
|
|||||||
try {
|
try {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push(`/account/${inputValue}`);
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
router.push(`${basePath}/account/${inputValue}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -66,13 +75,14 @@ const NodeAndAddressSearch = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if it's a node identity key
|
// Check if it's a node identity key
|
||||||
if (nymNodes) {
|
if (nsApiNodes) {
|
||||||
const matchingNode = nymNodes.find(
|
const matchingNode = nsApiNodes.find(
|
||||||
(node) => node.identity_key === inputValue
|
(node: NS_NODE) => node.identity_key === inputValue
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matchingNode) {
|
if (matchingNode) {
|
||||||
router.push(`/nym-node/${matchingNode.identity_key}`);
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
router.push(`${basePath}/nym-node/${matchingNode.identity_key}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,10 +114,8 @@ const NodeAndAddressSearch = () => {
|
|||||||
|
|
||||||
// Filter nodes by moniker if input is not empty
|
// Filter nodes by moniker if input is not empty
|
||||||
if (value.trim() !== "") {
|
if (value.trim() !== "") {
|
||||||
const filteredNodes = nymNodes.filter((node) =>
|
const filteredNodes = nsApiNodes.filter((node: NS_NODE) =>
|
||||||
node.self_description?.moniker
|
node.description.moniker?.toLowerCase().includes(value.toLowerCase())
|
||||||
?.toLowerCase()
|
|
||||||
.includes(value.toLowerCase())
|
|
||||||
);
|
);
|
||||||
setSearchOptions(filteredNodes);
|
setSearchOptions(filteredNodes);
|
||||||
} else {
|
} else {
|
||||||
@@ -118,11 +126,12 @@ const NodeAndAddressSearch = () => {
|
|||||||
// Handle node selection from dropdown
|
// Handle node selection from dropdown
|
||||||
const handleNodeSelect = (
|
const handleNodeSelect = (
|
||||||
event: React.SyntheticEvent,
|
event: React.SyntheticEvent,
|
||||||
value: string | IObservatoryNode | null
|
value: string | NS_NODE | null
|
||||||
) => {
|
) => {
|
||||||
if (value && typeof value !== "string") {
|
if (value && typeof value !== "string") {
|
||||||
setIsLoading(true); // Show loading spinner
|
setIsLoading(true); // Show loading spinner
|
||||||
router.push(`/nym-node/${value.node_id}`);
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
router.push(`${basePath}/nym-node/${value.node_id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,9 +141,9 @@ const NodeAndAddressSearch = () => {
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
freeSolo
|
freeSolo
|
||||||
options={searchOptions}
|
options={searchOptions}
|
||||||
getOptionLabel={(option: string | IObservatoryNode) => {
|
getOptionLabel={(option: string | NS_NODE) => {
|
||||||
if (typeof option === "string") return option;
|
if (typeof option === "string") return option;
|
||||||
return option.self_description?.moniker || "";
|
return option.description.moniker || "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
if (typeof option === "string" || typeof value === "string")
|
if (typeof option === "string" || typeof value === "string")
|
||||||
@@ -146,10 +155,10 @@ const NodeAndAddressSearch = () => {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
{...props}
|
{...props}
|
||||||
key={`${option.node_id}-${option.self_description?.moniker || ""}`}
|
key={`${option.node_id}-${option.description.moniker || ""}`}
|
||||||
style={{ fontSize: "0.875rem" }}
|
style={{ fontSize: "0.875rem" }}
|
||||||
>
|
>
|
||||||
{option.self_description?.moniker || "Unnamed Node"}
|
{option.description.moniker || "Unnamed Node"}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -182,7 +191,7 @@ const NodeAndAddressSearch = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={handleNodeSelect}
|
onChange={handleNodeSelect}
|
||||||
loading={isLoadingNodes}
|
loading={isNSApiNodesLoading}
|
||||||
loadingText="Loading nodes..."
|
loadingText="Loading nodes..."
|
||||||
noOptionsText="No nodes found"
|
noOptionsText="No nodes found"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { fetchOriginalStake } from "../../app/api";
|
|||||||
import { useNymClient } from "../../hooks/useNymClient";
|
import { useNymClient } from "../../hooks/useNymClient";
|
||||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
const OriginalStakeCard = () => {
|
const OriginalStakeCard = () => {
|
||||||
const { address } = useNymClient();
|
const { address } = useNymClient();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
// Use React Query to fetch original stake
|
// Use React Query to fetch original stake
|
||||||
const {
|
const {
|
||||||
@@ -18,14 +20,15 @@ const OriginalStakeCard = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["originalStake", address],
|
queryKey: ["originalStake", address, environment],
|
||||||
queryFn: () => fetchOriginalStake(address || ""),
|
queryFn: () => fetchOriginalStake(address || "", environment),
|
||||||
enabled: !!address, // Only fetch if address exists
|
enabled: !!address, // Only fetch if address exists
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return null; // Do not render if address is not available
|
return null; // Do not render if address is not available
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||||
import {
|
import {
|
||||||
type MRT_ColumnDef,
|
type MRT_ColumnDef,
|
||||||
@@ -37,11 +37,16 @@ import StakeActions from "./StakeActions";
|
|||||||
import StakeModal from "./StakeModal";
|
import StakeModal from "./StakeModal";
|
||||||
import type { MappedNymNode, MappedNymNodes } from "./StakeTableWithAction";
|
import type { MappedNymNode, MappedNymNodes } from "./StakeTableWithAction";
|
||||||
import { fee } from "./schemas";
|
import { fee } from "./schemas";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { getBasePathByEnv } from "../../../envs/config";
|
||||||
|
import { fetchStakerRewards } from "@/app/api";
|
||||||
|
import { IRewardDetails } from "@/app/api/types";
|
||||||
|
|
||||||
type DelegationWithNodeDetails = {
|
type DelegationWithNodeDetails = {
|
||||||
node: MappedNymNode | undefined;
|
node: MappedNymNode | undefined;
|
||||||
delegation: Delegation;
|
delegation: Delegation;
|
||||||
pendingEvent?: PendingEvent;
|
pendingEvent?: PendingEvent;
|
||||||
|
stakerReward?: IRewardDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnHeading = ({
|
const ColumnHeading = ({
|
||||||
@@ -82,7 +87,7 @@ const ColumnHeading = ({
|
|||||||
const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||||
const { nymClient, address, nymQueryClient } = useNymClient();
|
const { nymClient, address, nymQueryClient } = useNymClient();
|
||||||
const [delegations, setDelegations] = useState<DelegationWithNodeDetails[]>(
|
const [delegations, setDelegations] = useState<DelegationWithNodeDetails[]>(
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const [isDataLoading, setIsLoading] = useState(false);
|
const [isDataLoading, setIsLoading] = useState(false);
|
||||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||||
@@ -93,8 +98,11 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
identityKey: string;
|
identityKey: string;
|
||||||
}>();
|
}>();
|
||||||
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
||||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const { isWalletConnected } = useChain(chain);
|
||||||
const { data: pendingEvents } = usePendingEvents(nymQueryClient, address);
|
const { data: pendingEvents } = usePendingEvents(nymQueryClient, address);
|
||||||
|
const basePath = getBasePathByEnv(environment || "mainnet");
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
@@ -102,12 +110,27 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Use React Query to fetch total rewards
|
||||||
|
const {
|
||||||
|
data: stakerRewards = [],
|
||||||
|
isLoading: isStakerRewardsLoading,
|
||||||
|
isError: isStakerRewardsError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["stakerRewards", address, environment],
|
||||||
|
queryFn: () => fetchStakerRewards(address || "", environment),
|
||||||
|
enabled: !!address, // Only fetch if address exists
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
});
|
||||||
|
|
||||||
const handleRefetch = useCallback(async () => {
|
const handleRefetch = useCallback(async () => {
|
||||||
await queryClient.invalidateQueries();
|
await queryClient.invalidateQueries();
|
||||||
}, [queryClient]);
|
}, [queryClient]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!nymClient || !address || !nymQueryClient) return;
|
if (!nymClient || !address || !nymQueryClient || isStakerRewardsError)
|
||||||
|
return;
|
||||||
|
|
||||||
// Fetch staking data
|
// Fetch staking data
|
||||||
const fetchDelegations = async () => {
|
const fetchDelegations = async () => {
|
||||||
@@ -122,18 +145,23 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
delegations: Delegation[],
|
delegations: Delegation[],
|
||||||
nodes: MappedNymNode[],
|
nodes: MappedNymNode[],
|
||||||
pendingEvents: PendingEvent[] | undefined,
|
pendingEvents: PendingEvent[] | undefined,
|
||||||
|
stakerRewards: IRewardDetails[] | undefined
|
||||||
) => {
|
) => {
|
||||||
// Combine delegations with node details
|
// Combine delegations with node details
|
||||||
const delegationsWithNodeDetails = delegations.map((delegation) => {
|
const delegationsWithNodeDetails = delegations.map((delegation) => {
|
||||||
const node = nodes.find((node) => node.nodeId === delegation.node_id);
|
const node = nodes.find((node) => node.nodeId === delegation.node_id);
|
||||||
const pendingEvent = pendingEvents?.find(
|
const pendingEvent = pendingEvents?.find(
|
||||||
(event) => event?.mixId === delegation.node_id,
|
(event) => event?.mixId === delegation.node_id
|
||||||
|
);
|
||||||
|
const stakerReward = stakerRewards?.find(
|
||||||
|
(reward) => reward.node_id === delegation.node_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
delegation,
|
delegation,
|
||||||
pendingEvent,
|
pendingEvent,
|
||||||
|
stakerReward,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,7 +173,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
!delegationsWithNodeDetails.find(
|
!delegationsWithNodeDetails.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.node?.nodeId === e.mixId ||
|
item.node?.nodeId === e.mixId ||
|
||||||
item.delegation.node_id === e.mixId,
|
item.delegation.node_id === e.mixId
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
delegationsWithNodeDetails.push({
|
delegationsWithNodeDetails.push({
|
||||||
@@ -170,6 +198,18 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
node_id: e.mixId,
|
node_id: e.mixId,
|
||||||
owner: "-",
|
owner: "-",
|
||||||
},
|
},
|
||||||
|
stakerReward: {
|
||||||
|
amount_staked: {
|
||||||
|
amount: e.amount?.amount || "0",
|
||||||
|
denom: "unym",
|
||||||
|
},
|
||||||
|
node_id: e.mixId,
|
||||||
|
node_still_fully_bonded: true,
|
||||||
|
rewards: {
|
||||||
|
amount: "0",
|
||||||
|
denom: "unym",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,13 +226,22 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
delegations,
|
delegations,
|
||||||
nodes,
|
nodes,
|
||||||
pendingEvents,
|
pendingEvents,
|
||||||
|
stakerRewards
|
||||||
);
|
);
|
||||||
|
|
||||||
setDelegations(delegationsWithNodeDetails);
|
setDelegations(delegationsWithNodeDetails);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAndMapDelegations();
|
fetchAndMapDelegations();
|
||||||
}, [address, nodes, nymClient, nymQueryClient, pendingEvents]);
|
}, [
|
||||||
|
address,
|
||||||
|
nodes,
|
||||||
|
nymClient,
|
||||||
|
nymQueryClient,
|
||||||
|
pendingEvents,
|
||||||
|
stakerRewards,
|
||||||
|
environment,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleStakeOnNode = useCallback(
|
const handleStakeOnNode = useCallback(
|
||||||
async ({ nodeId, amount }: { nodeId: number; amount: string }) => {
|
async ({ nodeId, amount }: { nodeId: number; amount: string }) => {
|
||||||
@@ -206,7 +255,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
{ nodeId },
|
{ nodeId },
|
||||||
fee,
|
fee,
|
||||||
"Delegation from Nym Explorer V2",
|
"Delegation from Nym Explorer V2",
|
||||||
uNymFunds,
|
uNymFunds
|
||||||
);
|
);
|
||||||
setSelectedNodeForStaking(undefined);
|
setSelectedNodeForStaking(undefined);
|
||||||
|
|
||||||
@@ -235,7 +284,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
[nymClient, handleRefetch],
|
[nymClient, handleRefetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSelectStake = useCallback(
|
const handleOnSelectStake = useCallback(
|
||||||
@@ -266,7 +315,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isWalletConnected],
|
[isWalletConnected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUnstake = useCallback(
|
const handleUnstake = useCallback(
|
||||||
@@ -281,7 +330,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
nodeId,
|
nodeId,
|
||||||
},
|
},
|
||||||
fee,
|
fee,
|
||||||
`Explorer V2: Unstaking node ${nodeId}`,
|
`Explorer V2: Unstaking node ${nodeId}`
|
||||||
);
|
);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
await handleRefetch();
|
await handleRefetch();
|
||||||
@@ -306,7 +355,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[address, nymClient, handleRefetch],
|
[address, nymClient, handleRefetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleActionSelect = useCallback(
|
const handleActionSelect = useCallback(
|
||||||
@@ -322,7 +371,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleUnstake, handleOnSelectStake],
|
[handleUnstake, handleOnSelectStake]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTooltipTitle = useCallback(
|
const getTooltipTitle = useCallback(
|
||||||
@@ -333,13 +382,13 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
|
|
||||||
if (pending?.kind === "delegate") {
|
if (pending?.kind === "delegate") {
|
||||||
return `You have a delegation pending worth ${formatBigNum(
|
return `You have a delegation pending worth ${formatBigNum(
|
||||||
+pending.amount.amount / 1_000_000,
|
+pending.amount.amount / 1_000_000
|
||||||
)} NYM`;
|
)} NYM`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
[], // Add dependencies if necessary
|
[] // Add dependencies if necessary
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: MRT_ColumnDef<DelegationWithNodeDetails>[] = useMemo(
|
const columns: MRT_ColumnDef<DelegationWithNodeDetails>[] = useMemo(
|
||||||
@@ -349,10 +398,20 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
header: "",
|
header: "",
|
||||||
Header: <ColumnHeading>Name</ColumnHeading>,
|
Header: <ColumnHeading>Name</ColumnHeading>,
|
||||||
accessorKey: "node.name",
|
accessorKey: "node.name",
|
||||||
|
size: 220,
|
||||||
|
|
||||||
Cell: ({ row }) =>
|
Cell: ({ row }) =>
|
||||||
row.original.node?.name ? (
|
row.original.node?.name ? (
|
||||||
<Stack spacing={1}>
|
<Stack
|
||||||
|
spacing={1.5}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
>
|
||||||
<Typography variant="body4">{row.original.node.name}</Typography>
|
<Typography variant="body4">{row.original.node.name}</Typography>
|
||||||
|
{!row.original.stakerReward?.node_still_fully_bonded && (
|
||||||
|
<Chip size="small" label="Not bonded" />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
@@ -418,10 +477,10 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
|
|
||||||
sortingFn: (rowA, rowB) => {
|
sortingFn: (rowA, rowB) => {
|
||||||
const stakeA = Number.parseFloat(
|
const stakeA = Number.parseFloat(
|
||||||
rowA.original.delegation.amount.amount,
|
rowA.original.delegation.amount.amount
|
||||||
);
|
);
|
||||||
const stakeB = Number.parseFloat(
|
const stakeB = Number.parseFloat(
|
||||||
rowB.original.delegation.amount.amount,
|
rowB.original.delegation.amount.amount
|
||||||
);
|
);
|
||||||
return stakeA - stakeB;
|
return stakeA - stakeB;
|
||||||
},
|
},
|
||||||
@@ -452,6 +511,34 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
<Typography variant="body4">{0}%</Typography>
|
<Typography variant="body4">{0}%</Typography>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "rewards",
|
||||||
|
header: "Rewards",
|
||||||
|
accessorKey: "stakerReward.rewards.amount",
|
||||||
|
Header: <ColumnHeading>Rewards</ColumnHeading>,
|
||||||
|
size: 80,
|
||||||
|
|
||||||
|
sortingFn: (rowA, rowB) => {
|
||||||
|
const stakeA = Number.parseFloat(
|
||||||
|
rowA.original.stakerReward?.rewards.amount || "0"
|
||||||
|
);
|
||||||
|
const stakeB = Number.parseFloat(
|
||||||
|
rowB.original.stakerReward?.rewards.amount || "0"
|
||||||
|
);
|
||||||
|
return stakeA - stakeB;
|
||||||
|
},
|
||||||
|
Cell: ({ row }) =>
|
||||||
|
isStakerRewardsLoading ? (
|
||||||
|
<Typography variant="body4">Loading rewards</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body4">
|
||||||
|
{formatBigNum(
|
||||||
|
Number(row.original.stakerReward?.rewards?.amount) / 1_000_000
|
||||||
|
) || "0"}{" "}
|
||||||
|
NYM
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "Favorite",
|
id: "Favorite",
|
||||||
header: "Favorite",
|
header: "Favorite",
|
||||||
@@ -466,10 +553,10 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
),
|
),
|
||||||
sortingFn: (rowA, rowB) => {
|
sortingFn: (rowA, rowB) => {
|
||||||
const isFavoriteA = favorites.includes(
|
const isFavoriteA = favorites.includes(
|
||||||
rowA.original.node?.owner || "-",
|
rowA.original.node?.owner || "-"
|
||||||
);
|
);
|
||||||
const isFavoriteB = favorites.includes(
|
const isFavoriteB = favorites.includes(
|
||||||
rowB.original.node?.owner || "-",
|
rowB.original.node?.owner || "-"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort favorites first
|
// Sort favorites first
|
||||||
@@ -509,7 +596,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
handleActionSelect(
|
handleActionSelect(
|
||||||
action,
|
action,
|
||||||
row.original.delegation?.node_id,
|
row.original.delegation?.node_id,
|
||||||
row.original.node?.identity_key || undefined,
|
row.original.node?.identity_key || undefined
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -519,7 +606,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[handleActionSelect, favorites, getTooltipTitle],
|
[handleActionSelect, favorites, getTooltipTitle, isStakerRewardsLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
@@ -548,7 +635,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
size="large"
|
size="large"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Link href="/table" underline="none" color="inherit">
|
<Link href="/servers" underline="none" color="inherit">
|
||||||
Stake
|
Stake
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -651,7 +738,9 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
|||||||
},
|
},
|
||||||
muiTableBodyRowProps: ({ row }) => ({
|
muiTableBodyRowProps: ({ row }) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push(`/nym-node/${row.original.node?.nodeId || "not-found"}`);
|
router.push(
|
||||||
|
`${basePath}/nym-node/${row.original.node?.nodeId || "not-found"}`
|
||||||
|
);
|
||||||
},
|
},
|
||||||
hover: true,
|
hover: true,
|
||||||
sx: {
|
sx: {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||||
import { countryName } from "../../utils/countryName";
|
import { countryName } from "../../utils/countryName";
|
||||||
import StakeTable from "./StakeTable";
|
import StakeTable from "./StakeTable";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
// Utility function to calculate node saturation point
|
// Utility function to calculate node saturation point
|
||||||
function getNodeSaturationPoint(
|
function getNodeSaturationPoint(
|
||||||
totalStake: number,
|
totalStake: number,
|
||||||
stakeSaturationPoint: string,
|
stakeSaturationPoint: string
|
||||||
): number {
|
): number {
|
||||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||||
|
|
||||||
@@ -24,46 +24,58 @@ function getNodeSaturationPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map nodes with rewards data
|
// Map nodes with rewards data
|
||||||
const mappedNymNodes = (
|
const mappedNSApiNodes = (
|
||||||
nodes: IObservatoryNode[],
|
nodes: NS_NODE[],
|
||||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
epochRewardsData: ExplorerData["currentEpochRewardsData"]
|
||||||
) =>
|
) =>
|
||||||
nodes.map((node) => {
|
nodes
|
||||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
.map((node) => {
|
||||||
node.total_stake,
|
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||||
epochRewardsData.interval.stake_saturation_point,
|
+node.total_stake,
|
||||||
);
|
epochRewardsData.interval.stake_saturation_point
|
||||||
|
);
|
||||||
|
|
||||||
const cleanMoniker = DOMPurify.sanitize(
|
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||||
node.self_description.moniker,
|
/&/g,
|
||||||
).replace(/&/g, "&");
|
"&"
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: cleanMoniker,
|
name: cleanMoniker,
|
||||||
nodeId: node.node_id,
|
nodeId: node.node_id,
|
||||||
identity_key: node.identity_key,
|
identity_key: node.identity_key,
|
||||||
countryCode: node.description.auxiliary_details.location || null,
|
countryCode: node.geoip?.country || null,
|
||||||
countryName:
|
countryName: countryName(node.geoip?.country || null) || null,
|
||||||
countryName(node.description.auxiliary_details.location) || null,
|
profitMarginPercentage: node.rewarding_details
|
||||||
profitMarginPercentage:
|
? +node.rewarding_details.cost_params.profit_margin_percent * 100
|
||||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
: 0,
|
||||||
owner: node.bonding_address,
|
owner: node.bonding_address,
|
||||||
stakeSaturation: +nodeSaturationPoint || 0,
|
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];
|
export type MappedNymNode = MappedNymNodes[0];
|
||||||
|
|
||||||
const StakeTableWithAction = () => {
|
const StakeTableWithAction = () => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
// Use React Query to fetch epoch rewards
|
// Use React Query to fetch epoch rewards
|
||||||
const {
|
const {
|
||||||
data: epochRewardsData,
|
data: epochRewardsData,
|
||||||
isLoading: isEpochLoading,
|
isLoading: isEpochLoading,
|
||||||
isError: isEpochError,
|
isError: isEpochError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["epochRewards"],
|
queryKey: ["epochRewards", environment],
|
||||||
queryFn: fetchEpochRewards,
|
queryFn: () => fetchEpochRewards(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -72,12 +84,12 @@ const StakeTableWithAction = () => {
|
|||||||
|
|
||||||
// Use React Query to fetch Nym nodes
|
// Use React Query to fetch Nym nodes
|
||||||
const {
|
const {
|
||||||
data: nymNodes = [],
|
data: nsApiNodes = [],
|
||||||
isLoading: isNodesLoading,
|
isLoading: isNSApiNodesLoading,
|
||||||
isError: isNodesError,
|
isError: isNSApiNodesError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymNodes"],
|
queryKey: ["nsApiNodes", environment],
|
||||||
queryFn: fetchObservatoryNodes,
|
queryFn: () => fetchNSApiNodes(environment),
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -85,7 +97,7 @@ const StakeTableWithAction = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle loading state
|
// Handle loading state
|
||||||
if (isEpochLoading || isNodesLoading) {
|
if (isEpochLoading || isNSApiNodesLoading) {
|
||||||
return (
|
return (
|
||||||
<Card sx={{ height: "100%", mt: 5 }}>
|
<Card sx={{ height: "100%", mt: 5 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -99,7 +111,7 @@ const StakeTableWithAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle error state
|
// Handle error state
|
||||||
if (isEpochError || isNodesError) {
|
if (isEpochError || isNSApiNodesError) {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||||
@@ -115,9 +127,9 @@ const StakeTableWithAction = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
|
||||||
|
|
||||||
return <StakeTable nodes={data} />;
|
return <StakeTable nodes={nsApiNodesData} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StakeTableWithAction;
|
export default StakeTableWithAction;
|
||||||
|
|||||||
@@ -7,23 +7,19 @@ import { Button, Stack } from "@mui/material";
|
|||||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { fetchTotalStakerRewards } from "../../app/api";
|
import { fetchDelegations, fetchTotalStakerRewards } from "../../app/api";
|
||||||
import type { NodeRewardDetails } from "../../app/api/types";
|
import {
|
||||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../../config";
|
COSMOS_KIT_USE_CHAIN,
|
||||||
|
NYM_MIXNET_CONTRACT,
|
||||||
|
SANDBOX_MIXNET_CONTRACT_ADDRESS,
|
||||||
|
} from "../../config";
|
||||||
import { useNymClient } from "../../hooks/useNymClient";
|
import { useNymClient } from "../../hooks/useNymClient";
|
||||||
import Loading from "../loading";
|
import Loading from "../loading";
|
||||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||||
import RedeemRewardsModal from "../redeemRewards/RedeemRewardsModal";
|
import RedeemRewardsModal from "../redeemRewards/RedeemRewardsModal";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
import { SANDBOX_VALIDATOR_BASE_URL, VALIDATOR_BASE_URL } from "@/app/api/urls";
|
||||||
// Fetch delegations
|
// Fetch delegations
|
||||||
const fetchDelegations = async (
|
|
||||||
address: string,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
nymClient: any,
|
|
||||||
): Promise<Delegation[]> => {
|
|
||||||
const data = await nymClient.getDelegatorDelegations({ delegator: address });
|
|
||||||
return data.delegations;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SubHeaderRowActions = () => {
|
const SubHeaderRowActions = () => {
|
||||||
const [openRedeemRewardsModal, setOpenRedeemRewardsModal] =
|
const [openRedeemRewardsModal, setOpenRedeemRewardsModal] =
|
||||||
@@ -32,9 +28,18 @@ const SubHeaderRowActions = () => {
|
|||||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const mixnetContractAddress =
|
||||||
|
environment === "mainnet"
|
||||||
|
? NYM_MIXNET_CONTRACT
|
||||||
|
: SANDBOX_MIXNET_CONTRACT_ADDRESS;
|
||||||
|
|
||||||
|
const rpcAddress =
|
||||||
|
environment === "mainnet" ? VALIDATOR_BASE_URL : SANDBOX_VALIDATOR_BASE_URL;
|
||||||
|
|
||||||
const { address, nymClient } = useNymClient();
|
const { address, nymClient } = useNymClient();
|
||||||
const { getOfflineSigner } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { getOfflineSigner } = useChain(chain);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -44,30 +49,31 @@ const SubHeaderRowActions = () => {
|
|||||||
isLoading: isDelegationsLoading,
|
isLoading: isDelegationsLoading,
|
||||||
isError: isDelegationsError,
|
isError: isDelegationsError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["delegations", address],
|
queryKey: ["delegations", address, environment],
|
||||||
queryFn: () => fetchDelegations(address || "", nymClient),
|
queryFn: () => fetchDelegations(address || "", nymClient),
|
||||||
enabled: !!address && !!nymClient, // Only fetch if address and nymClient are available
|
enabled: !!address && !!nymClient, // Only fetch if address and nymClient exist
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch total rewards using React Query
|
// Fetch total rewards using React Query
|
||||||
const {
|
const {
|
||||||
data: totalStakerRewards = 0,
|
data: totalStakerRewards = 0,
|
||||||
isLoading: isRewardsLoading,
|
isLoading: isTotalStakerRewardsLoading,
|
||||||
isError: isRewardsError,
|
isError: isTotalStakerRewardsError,
|
||||||
refetch,
|
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["totalStakerRewards", address],
|
queryKey: ["totalStakerRewards", address, environment],
|
||||||
queryFn: () => fetchTotalStakerRewards(address || ""),
|
queryFn: () => fetchTotalStakerRewards(address || "", environment),
|
||||||
enabled: !!address, // Only fetch if address is available
|
enabled: !!address, // Only fetch if address exists
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRefetch = useCallback(async () => {
|
const handleRefetch = useCallback(async () => {
|
||||||
refetch();
|
await queryClient.invalidateQueries(); // This will refetch ALL active queries
|
||||||
queryClient.invalidateQueries(); // This will refetch ALL active queries
|
}, [queryClient]);
|
||||||
}, [queryClient, refetch]);
|
|
||||||
|
|
||||||
const handleRedeemRewards = useCallback(async () => {
|
const handleRedeemRewards = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -82,13 +88,13 @@ const SubHeaderRowActions = () => {
|
|||||||
const gasPrice = GasPrice.fromString("0.025unym");
|
const gasPrice = GasPrice.fromString("0.025unym");
|
||||||
|
|
||||||
const client = await SigningCosmWasmClient.connectWithSigner(
|
const client = await SigningCosmWasmClient.connectWithSigner(
|
||||||
"https://rpc.nymtech.net/",
|
rpcAddress,
|
||||||
signer,
|
signer,
|
||||||
{ gasPrice },
|
{ gasPrice }
|
||||||
);
|
);
|
||||||
|
|
||||||
const messages = delegations.map((delegation: NodeRewardDetails) => ({
|
const messages = delegations.map((delegation: Delegation) => ({
|
||||||
contractAddress: NYM_MIXNET_CONTRACT,
|
contractAddress: mixnetContractAddress,
|
||||||
funds: [],
|
funds: [],
|
||||||
msg: {
|
msg: {
|
||||||
withdraw_delegator_reward: {
|
withdraw_delegator_reward: {
|
||||||
@@ -101,7 +107,7 @@ const SubHeaderRowActions = () => {
|
|||||||
address,
|
address,
|
||||||
messages,
|
messages,
|
||||||
"auto",
|
"auto",
|
||||||
"Redeeming all rewards",
|
"Redeeming all rewards"
|
||||||
);
|
);
|
||||||
// Success state
|
// Success state
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -112,8 +118,11 @@ const SubHeaderRowActions = () => {
|
|||||||
tx: result?.transactionHash,
|
tx: result?.transactionHash,
|
||||||
|
|
||||||
onClose: async () => {
|
onClose: async () => {
|
||||||
await handleRefetch();
|
try {
|
||||||
setInfoModalProps({ open: false });
|
await handleRefetch();
|
||||||
|
} finally {
|
||||||
|
setInfoModalProps({ open: false });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -128,7 +137,15 @@ const SubHeaderRowActions = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [address, nymClient, delegations, handleRefetch, getOfflineSigner]);
|
}, [
|
||||||
|
address,
|
||||||
|
nymClient,
|
||||||
|
delegations,
|
||||||
|
handleRefetch,
|
||||||
|
getOfflineSigner,
|
||||||
|
mixnetContractAddress,
|
||||||
|
rpcAddress,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleRedeemRewardsButtonClick = () => {
|
const handleRedeemRewardsButtonClick = () => {
|
||||||
setOpenRedeemRewardsModal(true);
|
setOpenRedeemRewardsModal(true);
|
||||||
@@ -138,11 +155,11 @@ const SubHeaderRowActions = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDelegationsLoading || isRewardsLoading) {
|
if (isDelegationsLoading || isTotalStakerRewardsLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDelegationsError || isRewardsError) {
|
if (isDelegationsError || isTotalStakerRewardsError) {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||||
<Button variant="contained" disabled>
|
<Button variant="contained" disabled>
|
||||||
@@ -152,12 +169,13 @@ const SubHeaderRowActions = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleRedeemRewardsButtonClick}
|
onClick={handleRedeemRewardsButtonClick}
|
||||||
disabled={totalStakerRewards === 0}
|
disabled={totalStakerRewards / 1_000_000 < 0.1}
|
||||||
>
|
>
|
||||||
Redeem NYM
|
Redeem NYM
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { fetchTotalStakerRewards } from "../../app/api";
|
|||||||
import { useNymClient } from "../../hooks/useNymClient";
|
import { useNymClient } from "../../hooks/useNymClient";
|
||||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
const TotalRewardsCard = () => {
|
const TotalRewardsCard = () => {
|
||||||
const { address } = useNymClient();
|
const { address } = useNymClient();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
// Use React Query to fetch total rewards
|
// Use React Query to fetch total rewards
|
||||||
const {
|
const {
|
||||||
@@ -18,8 +20,8 @@ const TotalRewardsCard = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["totalStakerRewards", address],
|
queryKey: ["totalStakerRewards", address, environment],
|
||||||
queryFn: () => fetchTotalStakerRewards(address || ""),
|
queryFn: () => fetchTotalStakerRewards(address || "", environment),
|
||||||
enabled: !!address, // Only fetch if address exists
|
enabled: !!address, // Only fetch if address exists
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { fetchBalances } from "../../app/api";
|
|||||||
import { useNymClient } from "../../hooks/useNymClient";
|
import { useNymClient } from "../../hooks/useNymClient";
|
||||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||||
import ExplorerCard from "../cards/ExplorerCard";
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
const TotalStakeCard = () => {
|
const TotalStakeCard = () => {
|
||||||
const { address } = useNymClient();
|
const { address } = useNymClient();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
// Use React Query to fetch total stake
|
// Use React Query to fetch total stake
|
||||||
const {
|
const {
|
||||||
@@ -18,8 +20,8 @@ const TotalStakeCard = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["totalStake", address],
|
queryKey: ["totalStake", address, environment],
|
||||||
queryFn: () => fetchBalances(address || ""),
|
queryFn: () => fetchBalances(address || "", environment),
|
||||||
enabled: !!address, // Only fetch if address exists
|
enabled: !!address, // Only fetch if address exists
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
@@ -51,6 +53,7 @@ const TotalStakeCard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerCard label="Total Stake">
|
<ExplorerCard label="Total Stake">
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
import { Button, ButtonGroup, Stack } from "@mui/material";
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
label: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
value: "all" | "mixnodes" | "gateways" | "recommended";
|
||||||
|
};
|
||||||
|
|
||||||
|
type Options = 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;
|
||||||
@@ -14,15 +14,16 @@ import Cross from "../icons/Cross";
|
|||||||
import CrossDark from "../icons/CrossDark";
|
import CrossDark from "../icons/CrossDark";
|
||||||
import { WalletAddress } from "./WalletAddress";
|
import { WalletAddress } from "./WalletAddress";
|
||||||
import { WalletBalance } from "./WalletBalance";
|
import { WalletBalance } from "./WalletBalance";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
interface ButtonPropsWithOnClick extends ButtonProps {
|
interface ButtonPropsWithOnClick extends ButtonProps {
|
||||||
hideAddressAndBalance?: boolean;
|
hideAddressAndBalance?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectWallet = ({ ...buttonProps }: ButtonPropsWithOnClick) => {
|
const ConnectWallet = ({ ...buttonProps }: ButtonPropsWithOnClick) => {
|
||||||
const { connect, disconnect, address, isWalletConnected } =
|
const { environment } = useEnvironment();
|
||||||
useChain(COSMOS_KIT_USE_CHAIN);
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const { connect, disconnect, address, isWalletConnected } = useChain(chain);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const handleConnectWallet = async () => {
|
const handleConnectWallet = async () => {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import React from "react";
|
|||||||
import { Token } from "../../components/icons/Token";
|
import { Token } from "../../components/icons/Token";
|
||||||
import { TokenDark } from "../../components/icons/TokenDark";
|
import { TokenDark } from "../../components/icons/TokenDark";
|
||||||
import useGetWalletBalance from "../../hooks/useGetWalletBalance";
|
import useGetWalletBalance from "../../hooks/useGetWalletBalance";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
export const WalletBalance = () => {
|
export const WalletBalance = () => {
|
||||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const { isWalletConnected } = useChain(chain);
|
||||||
const { formattedBalance, isLoading, isError, refetch } =
|
const { formattedBalance, isLoading, isError, refetch } =
|
||||||
useGetWalletBalance();
|
useGetWalletBalance();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { scaleLinear } from "d3-scale";
|
||||||
|
import * as React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
ComposableMap,
|
||||||
|
Geographies,
|
||||||
|
Geography,
|
||||||
|
ZoomableGroup,
|
||||||
|
} from "react-simple-maps";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
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 { IconButton, Skeleton, Typography, Box } from "@mui/material";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type { CountryDataResponse } from "../../app/api/types";
|
||||||
|
import MAP_TOPOJSON from "../../assets/world-110m.json";
|
||||||
|
import ExplorerCard from "../cards/ExplorerCard";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
const mapPlaceholderDark = "/explorer/map-placeholder-dark.png";
|
||||||
|
const mapPlaceholderLight = "/explorer/map-placeholder-light.png";
|
||||||
|
|
||||||
|
export const WorldMap = (): JSX.Element => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
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", environment],
|
||||||
|
queryFn: () => fetchWorldMapCountries(environment),
|
||||||
|
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="Nym Server Locations">
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
color: isDarkMode ? "base.white" : "pine.950",
|
||||||
|
letterSpacing: 0.7,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{uniqueLocations}
|
||||||
|
</Typography>
|
||||||
|
</ExplorerCard>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
export const COSMOS_KIT_USE_CHAIN =
|
export const COSMOS_KIT_USE_CHAIN =
|
||||||
process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || "nyx";
|
process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || "sandbox";
|
||||||
|
|
||||||
export const NYM_MIXNET_CONTRACT =
|
export const NYM_MIXNET_CONTRACT =
|
||||||
process.env.NYM_MIXNET_CONTRACT ||
|
process.env.NYM_MIXNET_CONTRACT ||
|
||||||
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
|
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
|
||||||
|
|
||||||
|
export const SANDBOX_MIXNET_CONTRACT_ADDRESS =
|
||||||
|
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"iconLight": "explorerCard",
|
"iconLight": "explorerCard",
|
||||||
"iconDark": "explorerCardDark",
|
"iconDark": "explorerCardDark",
|
||||||
"image": "/explorer/images/Network.webp",
|
"image": "/explorer/images/Network.webp",
|
||||||
|
"link": "https://nym.com/blog/welcome-to-explorer",
|
||||||
"overview": {
|
"overview": {
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"iconLight": "stakeCard",
|
"iconLight": "stakeCard",
|
||||||
"iconDark": "stakeCardDark",
|
"iconDark": "stakeCardDark",
|
||||||
"image": "/explorer/images/stake-article.webp",
|
"image": "/explorer/images/stake-article.webp",
|
||||||
|
"link": "https://nym.com/blog/stake-Nym-tokens",
|
||||||
"overview": {
|
"overview": {
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { PendingEpochEventKind } from "@nymproject/contract-clients/Mixnet.types";
|
import type { PendingEpochEventKind } from "@nymproject/contract-clients/Mixnet.types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEnvironment } from "../providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const getEventsByAddress = (
|
export const getEventsByAddress = (kind: PendingEpochEventKind, address: string) => {
|
||||||
kind: PendingEpochEventKind,
|
|
||||||
address: string,
|
|
||||||
) => {
|
|
||||||
if ("delegate" in kind && kind.delegate.owner === address) {
|
if ("delegate" in kind && kind.delegate.owner === address) {
|
||||||
return {
|
return {
|
||||||
kind: "delegate" as const,
|
kind: "delegate" as const,
|
||||||
@@ -27,8 +25,10 @@ export type PendingEvent = ReturnType<typeof getEventsByAddress>;
|
|||||||
// Custom Hook for fetching pending events
|
// Custom Hook for fetching pending events
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const usePendingEvents = (nymQueryClient: any, address: string | undefined) => {
|
const usePendingEvents = (nymQueryClient: any, address: string | undefined) => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["pendingEvents", address], // Query key to uniquely identify this query
|
queryKey: ["pendingEvents", address, environment], // Query key to uniquely identify this query
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!nymQueryClient || !address) {
|
if (!nymQueryClient || !address) {
|
||||||
throw new Error("Missing required dependencies");
|
throw new Error("Missing required dependencies");
|
||||||
@@ -47,6 +47,9 @@ const usePendingEvents = (nymQueryClient: any, address: string | undefined) => {
|
|||||||
return pendingEvents;
|
return pendingEvents;
|
||||||
},
|
},
|
||||||
enabled: !!nymQueryClient && !!address, // Prevents execution if dependencies are missing
|
enabled: !!nymQueryClient && !!address, // Prevents execution if dependencies are missing
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useChain } from "@cosmos-kit/react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { COSMOS_KIT_USE_CHAIN } from "../config";
|
import { COSMOS_KIT_USE_CHAIN } from "../config";
|
||||||
import { unymToNym } from "../utils/currency";
|
import { unymToNym } from "../utils/currency";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const fetchNYMBalance = async (address: string, getCosmWasmClient: any) => {
|
const fetchNYMBalance = async (address: string, getCosmWasmClient: any) => {
|
||||||
@@ -17,7 +18,10 @@ const fetchNYMBalance = async (address: string, getCosmWasmClient: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useGetWalletBalance = () => {
|
const useGetWalletBalance = () => {
|
||||||
const { getCosmWasmClient, address } = useChain(COSMOS_KIT_USE_CHAIN);
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
|
||||||
|
const { getCosmWasmClient, address } = useChain(chain);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data = { NYMBalance: "0", formattedBalance: "-" },
|
data = { NYMBalance: "0", formattedBalance: "-" },
|
||||||
@@ -25,7 +29,7 @@ const useGetWalletBalance = () => {
|
|||||||
isError,
|
isError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["nymBalance", address],
|
queryKey: ["nymBalance", address, environment],
|
||||||
queryFn: () => fetchNYMBalance(address || "", getCosmWasmClient),
|
queryFn: () => fetchNYMBalance(address || "", getCosmWasmClient),
|
||||||
enabled: !!address, // Only fetch if address exists
|
enabled: !!address, // Only fetch if address exists
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,14 +7,25 @@ import type {
|
|||||||
MixnetQueryClient,
|
MixnetQueryClient,
|
||||||
} from "@nymproject/contract-clients/Mixnet.client";
|
} from "@nymproject/contract-clients/Mixnet.client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../config";
|
import {
|
||||||
|
COSMOS_KIT_USE_CHAIN,
|
||||||
|
NYM_MIXNET_CONTRACT,
|
||||||
|
SANDBOX_MIXNET_CONTRACT_ADDRESS,
|
||||||
|
} from "../config";
|
||||||
|
import { useEnvironment } from "@/providers/EnvironmentProvider";
|
||||||
|
|
||||||
export const useNymClient = () => {
|
export const useNymClient = () => {
|
||||||
const [nymClient, setNymClient] = useState<MixnetClient>();
|
const [nymClient, setNymClient] = useState<MixnetClient>();
|
||||||
const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>();
|
const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>();
|
||||||
|
const { environment } = useEnvironment();
|
||||||
|
const chain = environment === "mainnet" ? COSMOS_KIT_USE_CHAIN : "sandbox";
|
||||||
|
const mixnetContractAddress =
|
||||||
|
environment === "mainnet"
|
||||||
|
? NYM_MIXNET_CONTRACT
|
||||||
|
: SANDBOX_MIXNET_CONTRACT_ADDRESS;
|
||||||
|
|
||||||
const { address, getCosmWasmClient, getSigningCosmWasmClient } =
|
const { address, getCosmWasmClient, getSigningCosmWasmClient } =
|
||||||
useChain(COSMOS_KIT_USE_CHAIN);
|
useChain(chain);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (address) {
|
if (address) {
|
||||||
@@ -26,13 +37,13 @@ export const useNymClient = () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
cosmWasmSigningClient as any,
|
cosmWasmSigningClient as any,
|
||||||
address,
|
address,
|
||||||
NYM_MIXNET_CONTRACT,
|
mixnetContractAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryClient = new contracts.Mixnet.MixnetQueryClient(
|
const queryClient = new contracts.Mixnet.MixnetQueryClient(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
cosmWasmClient as any,
|
cosmWasmClient as any,
|
||||||
NYM_MIXNET_CONTRACT,
|
mixnetContractAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
setNymClient(client);
|
setNymClient(client);
|
||||||
@@ -41,7 +52,13 @@ export const useNymClient = () => {
|
|||||||
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
}, [address, getCosmWasmClient, getSigningCosmWasmClient]);
|
}, [
|
||||||
|
address,
|
||||||
|
getCosmWasmClient,
|
||||||
|
getSigningCosmWasmClient,
|
||||||
|
mixnetContractAddress,
|
||||||
|
environment,
|
||||||
|
]);
|
||||||
|
|
||||||
return { nymClient, nymQueryClient, address };
|
return { nymClient, nymQueryClient, address };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export type Environment = "mainnet" | "sandbox";
|
||||||
|
|
||||||
|
interface EnvironmentContextType {
|
||||||
|
environment: Environment;
|
||||||
|
setEnvironment: (env: Environment) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentContext = createContext<EnvironmentContextType | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EnvironmentProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [environment, setEnvironment] = useState<Environment>("mainnet");
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Initialize environment from URL path
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname.startsWith("/sandbox-explorer")) {
|
||||||
|
setEnvironment("sandbox");
|
||||||
|
} else if (pathname.startsWith("/explorer")) {
|
||||||
|
setEnvironment("mainnet");
|
||||||
|
} else {
|
||||||
|
// Default to mainnet for other paths
|
||||||
|
setEnvironment("mainnet");
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentContext.Provider value={{ environment, setEnvironment }}>
|
||||||
|
{children}
|
||||||
|
</EnvironmentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEnvironment = () => {
|
||||||
|
const context = useContext(EnvironmentContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"useEnvironment must be used within an EnvironmentProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import { fetchCurrentEpoch } from "@/app/api";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { differenceInMilliseconds } from "date-fns";
|
import { differenceInMilliseconds } from "date-fns";
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { useEnvironment } from "./EnvironmentProvider";
|
||||||
type EpochStatus = "active" | "pending";
|
type EpochStatus = "active" | "pending";
|
||||||
|
|
||||||
export type EpochResponseData =
|
export type EpochResponseData =
|
||||||
@@ -45,14 +45,15 @@ const useEpochContext = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EpochProvider = ({ children }: { children: React.ReactNode }) => {
|
const EpochProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const { environment } = useEnvironment();
|
||||||
const [epochStatus, setEpochStatus] = useState<EpochStatus>("pending");
|
const [epochStatus, setEpochStatus] = useState<EpochStatus>("pending");
|
||||||
|
|
||||||
const QueryClient = useQueryClient();
|
const QueryClient = useQueryClient();
|
||||||
|
|
||||||
const { data, isError, isLoading } = useQuery({
|
const { data, isError, isLoading } = useQuery({
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
queryKey: ["currentEpoch"],
|
queryKey: ["currentEpoch", environment],
|
||||||
queryFn: fetchCurrentEpoch,
|
queryFn: () => fetchCurrentEpoch(environment),
|
||||||
refetchInterval: ({ state }) => {
|
refetchInterval: ({ state }) => {
|
||||||
// refetchInterval can be set dynamically based on the current state
|
// refetchInterval can be set dynamically based on the current state
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ const EpochProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isEpochTimeValid = checkIsEpochTimeValid(
|
const isEpochTimeValid = checkIsEpochTimeValid(
|
||||||
state.data.current_epoch_end.toString(),
|
state.data.current_epoch_end.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
// if epoch time is not valid (i.e current_time > epoch_start_time) refetch in 30 secs
|
// if epoch time is not valid (i.e current_time > epoch_start_time) refetch in 30 secs
|
||||||
@@ -73,7 +74,7 @@ const EpochProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
// if epoch time is valid, refetch based on the epoch end time
|
// if epoch time is valid, refetch based on the epoch end time
|
||||||
const newRefetchInterval = calculateRefetchInterval(
|
const newRefetchInterval = calculateRefetchInterval(
|
||||||
state.data.current_epoch_end.toString(),
|
state.data.current_epoch_end.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
setEpochStatus("active");
|
setEpochStatus("active");
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import CosmosKitProvider from "./CosmosKitProvider";
|
import CosmosKitProvider from "./CosmosKitProvider";
|
||||||
|
import { EnvironmentProvider } from "./EnvironmentProvider";
|
||||||
import { EpochProvider } from "./EpochProvider";
|
import { EpochProvider } from "./EpochProvider";
|
||||||
import { QueryProvider } from "./QueryProvider";
|
import { QueryProvider } from "./QueryProvider";
|
||||||
import ThemeProvider from "./ThemeProvider";
|
import ThemeProvider from "./ThemeProvider";
|
||||||
|
|
||||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<EnvironmentProvider>
|
||||||
<QueryProvider>
|
<ThemeProvider>
|
||||||
<EpochProvider>
|
<QueryProvider>
|
||||||
<CosmosKitProvider>{children}</CosmosKitProvider>
|
<EpochProvider>
|
||||||
</EpochProvider>
|
<CosmosKitProvider>{children}</CosmosKitProvider>
|
||||||
</QueryProvider>
|
</EpochProvider>
|
||||||
</ThemeProvider>
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</EnvironmentProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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';
|
||||||
+14
-11
@@ -9,6 +9,7 @@ import CosmosKitProvider from './context/cosmos-kit';
|
|||||||
import '@interchain-ui/react/styles';
|
import '@interchain-ui/react/styles';
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import { WalletProvider } from './context/wallet';
|
import { WalletProvider } from './context/wallet';
|
||||||
|
import { EnvironmentProvider } from './providers/EnvironmentProvider';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
const elem = document.getElementById('app');
|
const elem = document.getElementById('app');
|
||||||
@@ -17,17 +18,19 @@ if (elem) {
|
|||||||
const root = createRoot(elem);
|
const root = createRoot(elem);
|
||||||
root.render(
|
root.render(
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryContent}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryContent}>
|
||||||
<MainContextProvider>
|
<EnvironmentProvider>
|
||||||
<CosmosKitProvider>
|
<MainContextProvider>
|
||||||
<WalletProvider>
|
<CosmosKitProvider>
|
||||||
<NetworkExplorerThemeProvider>
|
<WalletProvider>
|
||||||
<Router>
|
<NetworkExplorerThemeProvider>
|
||||||
<App />
|
<Router>
|
||||||
</Router>
|
<App />
|
||||||
</NetworkExplorerThemeProvider>
|
</Router>
|
||||||
</WalletProvider>
|
</NetworkExplorerThemeProvider>
|
||||||
</CosmosKitProvider>
|
</WalletProvider>
|
||||||
</MainContextProvider>
|
</CosmosKitProvider>
|
||||||
|
</MainContextProvider>
|
||||||
|
</EnvironmentProvider>
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user