Yana/pending staking events (#5400)
* Add pending events to stake table * Add additional tanstack refetch on user stake/unstake/redeem rewards * refactor repeated fetch functions * clean up fetching functions * refactor & clean up * Add epoch waiting message on epoch change delay * fine tune epoch change * clean up * refactor imports * Add transaction hash to successful redeem rewards InfoModal * fix epoch time check and mobile header * Fix Loading modal width on mobile * fix epoch logic * clean up * clean up logs * add waiting for epoch to start to landing page * clean up * Add refetch of all queries on epoch change * Finalise state change on epoch change * clean up * fix build * Fix NodesTable mobile view * Fix stake table mobile view * fix typo * Fix blog articles height * Add loading skeletons to landing page cards * clean up * Add skeletons to cards * Add skeletons, and loading/error refetch on wallet balnce * clean up * Add active stakers card * clean up * change NGM to mixnet * Add TVL to Tokenomics card * Add last total stake to Stake Card * clean up * Fix stake sorting function in Stake Table * Add wrap of identity key and address to Basic Info Card * Add counter to epoch time on staking page * clean up * update epoch labels * Add circular loading on Toggle Button * Update Toggle button loading functionality * Add skeletons to account cards * Add search functionality on Enter * clean up * DOMpurify node name and description * Add column with id and identity key, wrap names to 2 lines * Set width of column headers to 110px * fix pending events for delegations * Fix Stake button proppagation * Add full country name to tooltips * Take out connect wallet from mobile menu toggle * finetune epoch change intervals * Add error text to Magic Search * fix build * Add react-markdown for Blog articles * fix graph's width and Table column headings * fix Magic Search loading * Fix grid on account page * fix account card address width * Fix permanent loading spinner on ToggleButton * clean up URL's, fix copy address on the Basic Card * replace mintscan with ping, open tx link on new page * Take out toggle button if no node bonded by address * Set fixed column width on tables * Add not-found page to account, when no node bonded * Add full country name to tables and node profile card * clean up * Table fixes * Fix sorting in Delegations table Node page * clean up * Fix line chart view * refactor epoch progress bar * remove unused imports * remove tanstack delclaration module * create epoch data provider * remove logic from togglebutton component * use epoch provider in components * invalidateQueries should be awaited * tidy up QualityIndicatorsCard component formatting * fix infinite loop in epoch provider --------- Co-authored-by: Yana <yanok87@users.noreply.github.com> Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
This commit is contained in:
@@ -28,11 +28,13 @@
|
||||
"@nymproject/react": "1.0.0",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
"@tanstack/react-query-devtools": "^5.64.2",
|
||||
"@tanstack/react-query-next-experimental": "^5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"chain-registry": "^1.69.64",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"isomorphic-dompurify": "^2.21.0",
|
||||
"material-react-table": "^3.0.3",
|
||||
"next": "15.0.3",
|
||||
"qrcode.react": "^4.1.0",
|
||||
@@ -45,6 +47,7 @@
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-data-grid": "7.1.1",
|
||||
"@mui/x-charts": "^7.22.3",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"zod": "^3.24.1"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
|
||||
const getNymNodes = async (): Promise<IObservatoryNode[]> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_NODES_URL}`, {
|
||||
next: {
|
||||
revalidate: 900,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export default getNymNodes;
|
||||
@@ -0,0 +1,57 @@
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ address: string }>;
|
||||
}) {
|
||||
try {
|
||||
const address = (await params).address;
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: true,
|
||||
link: `/account/${address}/not-found/`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: `/account/${address}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h5">
|
||||
Is this your accont? Set up your node!
|
||||
</Typography>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards limit={4} />
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
let errorMessage = "An error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { IAccountBalancesInfo, NymTokenomics } from "@/app/api/types";
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_ACCOUNT_ADDRESS, NYM_NODES, NYM_PRICES_API } from "@/app/api/urls";
|
||||
import { AccountBalancesCard } from "@/components/accountPageComponents/AccountBalancesCard";
|
||||
import { AccountInfoCard } from "@/components/accountPageComponents/AccountInfoCard";
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { fetchNodes } from "@/app/api";
|
||||
import type { NodeData } from "@/app/api/types";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { AccountBalancesCard } from "../../../../components/accountPageComponents/AccountBalancesCard";
|
||||
import { AccountInfoCard } from "../../../../components/accountPageComponents/AccountInfoCard";
|
||||
import { ContentLayout } from "../../../../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../../../../components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "../../../../components/toggleButton/ToggleButton";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
@@ -16,56 +14,21 @@ export default async function Account({
|
||||
params: Promise<{ address: string }>;
|
||||
}) {
|
||||
try {
|
||||
const { address } = await params;
|
||||
const address = (await params).address;
|
||||
|
||||
const accountData = await fetch(`${NYM_ACCOUNT_ADDRESS}${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const response = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymNodes: NodeData[] = await response.json();
|
||||
const nymNodes: NodeData[] = await fetchNodes();
|
||||
|
||||
const nymNode = nymNodes.find(
|
||||
(node) => node.bond_information.owner === address,
|
||||
);
|
||||
|
||||
const nymAccountBalancesData: IAccountBalancesInfo =
|
||||
await accountData.json();
|
||||
|
||||
if (!nymAccountBalancesData) {
|
||||
return <Typography>Account not found</Typography>;
|
||||
}
|
||||
|
||||
const nymPrice = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymPriceData: NymTokenomics = await nymPrice.json();
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={6}>
|
||||
<SectionHeading title="Account Details" />
|
||||
</Grid>
|
||||
|
||||
<Grid size={6} justifyContent="flex-end">
|
||||
<Box sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<ExplorerButtonGroup
|
||||
@@ -75,7 +38,7 @@ export default async function Account({
|
||||
isSelected: false,
|
||||
link: nymNode
|
||||
? `/nym-node/${nymNode.node_id}`
|
||||
: "/nym-node/not-found",
|
||||
: `/account/${address}/not-found`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
@@ -86,22 +49,14 @@ export default async function Account({
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<AccountInfoCard accountInfo={nymAccountBalancesData} />
|
||||
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<AccountInfoCard address={address} />
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<AccountBalancesCard
|
||||
accountInfo={nymAccountBalancesData}
|
||||
nymPrice={nymPriceData.quotes.USD.price}
|
||||
/>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<AccountBalancesCard address={address} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards limit={4} />
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import CardSkeleton from "@/components/cards/Skeleton";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
<SectionHeading title="Explorer" />
|
||||
<Suspense fallback={<CardSkeleton sx={{ mt: 5 }} />}>
|
||||
<Box sx={{ mt: 5 }}>
|
||||
<NodeTableWithAction />
|
||||
</Suspense>
|
||||
</Box>
|
||||
<Grid container columnSpacing={5} rowSpacing={5} mt={10}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { fetchNodeInfo } from "@/app/api";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
@@ -22,25 +20,7 @@ export default async function NymNode({
|
||||
try {
|
||||
const id = Number((await params).id);
|
||||
|
||||
const observatoryResponse = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const observatoryNymNodes: IObservatoryNode[] =
|
||||
await observatoryResponse.json();
|
||||
|
||||
if (!observatoryNymNodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const observatoryNymNode = observatoryNymNodes.find(
|
||||
(node) => node.node_id === id,
|
||||
);
|
||||
const observatoryNymNode = await fetchNodeInfo(id);
|
||||
|
||||
if (!observatoryNymNode) {
|
||||
return null;
|
||||
@@ -126,12 +106,6 @@ export default async function NymNode({
|
||||
<NodeChatCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards limit={4} />
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<SectionHeading title="Nym Node" />
|
||||
<Typography variant="body3">This Nym Node could not be found</Typography>
|
||||
</ContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
@@ -9,6 +9,7 @@ import { Box, Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { format } from "date-fns";
|
||||
import Image from "next/image";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default async function BlogPage({
|
||||
params,
|
||||
@@ -104,7 +105,7 @@ export default async function BlogPage({
|
||||
<SectionHeading title={section.heading} />
|
||||
{section.text.map(({ text }) => (
|
||||
<Typography key={text} variant="body2" sx={{ mt: 3 }}>
|
||||
{text}
|
||||
<Markdown>{text}</Markdown>
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import OverviewCards from "@/components/staking/OverviewCards";
|
||||
import StakeTableWithAction from "@/components/staking/StakeTableWithAction";
|
||||
import SubHeaderRow from "@/components/staking/SubHeaderRow";
|
||||
import { ContentLayout } from "../../../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../../../components/headings/SectionHeading";
|
||||
import OverviewCards from "../../../components/staking/OverviewCards";
|
||||
import StakeTableWithAction from "../../../components/staking/StakeTableWithAction";
|
||||
import SubHeaderRow from "../../../components/staking/SubHeaderRow";
|
||||
|
||||
export default async function StakingPage() {
|
||||
return (
|
||||
|
||||
@@ -1,218 +1,38 @@
|
||||
import { addSeconds } from "date-fns";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
CurrentEpochData,
|
||||
ExplorerData,
|
||||
GatewayStatus,
|
||||
IAccountBalancesInfo,
|
||||
IObservatoryNode,
|
||||
IPacketsAndStakingData,
|
||||
NodeData,
|
||||
NymTokenomics,
|
||||
ObservatoryBalance,
|
||||
} from "./types";
|
||||
import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_BALANCES_URL,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
NYM_NODES_DESCRIBED,
|
||||
NYM_NODE_DESCRIPTION,
|
||||
NYM_ACCOUNT_ADDRESS,
|
||||
NYM_NODES,
|
||||
NYM_PRICES_API,
|
||||
OBSERVATORY_GATEWAYS_URL,
|
||||
} from "./urls";
|
||||
|
||||
type Denom = "unym" | "nym";
|
||||
|
||||
export interface IPacketsAndStakingData {
|
||||
date_utc: string;
|
||||
total_packets_received: number;
|
||||
total_packets_sent: number;
|
||||
total_packets_dropped: number;
|
||||
total_stake: number;
|
||||
}
|
||||
|
||||
export interface CurrentEpochData {
|
||||
id: number;
|
||||
current_epoch_id: number;
|
||||
current_epoch_start: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
epochs_in_interval: number;
|
||||
total_elapsed_epochs: number;
|
||||
}
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: {
|
||||
circulating_supply: { denom: Denom; amount: string };
|
||||
mixmining_reserve: { denom: Denom; amount: string };
|
||||
total_supply: { denom: Denom; amount: string };
|
||||
vesting_tokens: { denom: Denom; amount: string };
|
||||
};
|
||||
nymNodesData: {
|
||||
gateways: {
|
||||
bonded: { count: number; last_updated_utc: string };
|
||||
blacklisted: { count: number; last_updated_utc: string };
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
explorer: { count: number; last_updated_utc: string };
|
||||
};
|
||||
mixnodes: {
|
||||
bonded: {
|
||||
count: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
reserve: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
blacklisted: {
|
||||
count: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
};
|
||||
};
|
||||
packetsAndStakingData: IPacketsAndStakingData[];
|
||||
|
||||
currentEpochRewardsData: {
|
||||
interval: {
|
||||
reward_pool: string;
|
||||
staking_supply: string;
|
||||
staking_supply_scale_factor: string;
|
||||
epoch_reward_budget: string;
|
||||
stake_saturation_point: string;
|
||||
active_set_work_factor: string;
|
||||
interval_pool_emission: string;
|
||||
sybil_resistance: string;
|
||||
};
|
||||
rewarded_set: {
|
||||
entry_gateways: number;
|
||||
exit_gateways: number;
|
||||
mixnodes: number;
|
||||
standby: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
|
||||
|
||||
export interface ExplorerCache {
|
||||
explorerCache?: {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(NYM_NODES_DESCRIBED, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CIRCULATING NYM SUPPLY
|
||||
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH PACKETS AND STAKING
|
||||
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const fetchNymNodeDescription = await fetch(NYM_NODE_DESCRIPTION, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
nymNodeDescriptionRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchNymNodeDescription,
|
||||
]);
|
||||
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
nymNodeDescriptionData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
nymNodeDescriptionRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
nymNodeDescriptionData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
// makes sure the cache exists in global memory
|
||||
let doUpdate = false;
|
||||
const now = new Date();
|
||||
if (!(global as ExplorerCache).explorerCache) {
|
||||
(global as any).explorerCache = {};
|
||||
doUpdate = true;
|
||||
}
|
||||
if (
|
||||
(global as ExplorerCache)?.explorerCache?.lastUpdated &&
|
||||
now.getDate() - (global as any).explorerCache.lastUpdated.getDate() >
|
||||
CACHE_TIME_SECONDS
|
||||
) {
|
||||
doUpdate = true;
|
||||
}
|
||||
|
||||
// if the cache has expired or never existed, get it from API's
|
||||
if (doUpdate) {
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await getExplorerData();
|
||||
|
||||
packetsAndStakingData.pop();
|
||||
|
||||
(global as any).explorerCache.data = {
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
};
|
||||
(global as any).explorerCache.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheExplorerData() {
|
||||
await ensureCacheExists();
|
||||
|
||||
if (!(global as ExplorerCache).explorerCache?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (global as ExplorerCache)?.explorerCache?.data || null;
|
||||
}
|
||||
|
||||
export const fetchEpochRewards = async () => {
|
||||
// Fetch function for epoch rewards
|
||||
export const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
cache: "no-store", // Ensures fresh data on every request
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -222,7 +42,22 @@ export const fetchEpochRewards = async () => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchObservatoryNodes = async () => {
|
||||
// Fetch gateway status based on identity key
|
||||
export const fetchGatewayStatus = async (
|
||||
identityKey: string,
|
||||
): Promise<GatewayStatus | null> => {
|
||||
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch gateway status");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchNodeInfo = async (
|
||||
id: number,
|
||||
): Promise<IObservatoryNode | undefined> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
@@ -234,7 +69,8 @@ export const fetchObservatoryNodes = async () => {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
return nodes.find((node) => node.node_id === id);
|
||||
};
|
||||
|
||||
export const fetchNodeDelegations = async (id: number) => {
|
||||
@@ -254,3 +90,165 @@ export const fetchNodeDelegations = async (id: number) => {
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchCurrentEpoch = async () => {
|
||||
const response = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
cache: "no-store", // Ensures fresh data on every request
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch current epoch data");
|
||||
}
|
||||
|
||||
const data: CurrentEpochData = await response.json();
|
||||
const epochEndTime = addSeconds(
|
||||
new Date(data.current_epoch_start),
|
||||
data.epoch_length.secs,
|
||||
).toISOString();
|
||||
|
||||
return { ...data, current_epoch_end: epochEndTime };
|
||||
};
|
||||
|
||||
// Fetch balances based on the address
|
||||
export const fetchBalances = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Calculate total stake
|
||||
return (
|
||||
Number(balances.rewards.staking_rewards.amount) +
|
||||
Number(balances.delegated.amount)
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch function to get total staker rewards
|
||||
export const fetchTotalStakerRewards = async (
|
||||
address: string,
|
||||
): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the staking rewards amount
|
||||
return Number(balances.rewards.staking_rewards.amount);
|
||||
};
|
||||
|
||||
// Fetch function to get the original stake
|
||||
export const fetchOriginalStake = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the delegated amount
|
||||
return Number(balances.delegated.amount);
|
||||
};
|
||||
|
||||
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
||||
const response = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const data: IPacketsAndStakingData[] = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
// Fetch Account Balance
|
||||
export const fetchAccountBalance = async (
|
||||
address: string,
|
||||
): Promise<IAccountBalancesInfo> => {
|
||||
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch account balance error from api");
|
||||
}
|
||||
|
||||
const data: IAccountBalancesInfo = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
// 🔹 Fetch Nodes
|
||||
export const fetchNodes = async (): Promise<NodeData[]> => {
|
||||
const res = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch nodes");
|
||||
}
|
||||
const data: NodeData[] = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchObservatoryNodes = async (): Promise<
|
||||
IObservatoryNode[] | null
|
||||
> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 🔹 Fetch NYM Price
|
||||
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||
const res = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch NYM price");
|
||||
}
|
||||
const data: NymTokenomics = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,75 @@ export type API_RESPONSE<T> = {
|
||||
data: T[];
|
||||
};
|
||||
|
||||
export type Denom = "unym" | "nym";
|
||||
|
||||
export interface IPacketsAndStakingData {
|
||||
date_utc: string;
|
||||
total_packets_received: number;
|
||||
total_packets_sent: number;
|
||||
total_packets_dropped: number;
|
||||
total_stake: number;
|
||||
}
|
||||
|
||||
export interface CurrentEpochData {
|
||||
id: number;
|
||||
current_epoch_id: number;
|
||||
current_epoch_start: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
epochs_in_interval: number;
|
||||
total_elapsed_epochs: number;
|
||||
}
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: {
|
||||
circulating_supply: { denom: Denom; amount: string };
|
||||
mixmining_reserve: { denom: Denom; amount: string };
|
||||
total_supply: { denom: Denom; amount: string };
|
||||
vesting_tokens: { denom: Denom; amount: string };
|
||||
};
|
||||
nymNodesData: {
|
||||
gateways: {
|
||||
bonded: { count: number; last_updated_utc: string };
|
||||
blacklisted: { count: number; last_updated_utc: string };
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
explorer: { count: number; last_updated_utc: string };
|
||||
};
|
||||
mixnodes: {
|
||||
bonded: {
|
||||
count: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
reserve: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
blacklisted: {
|
||||
count: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
};
|
||||
};
|
||||
packetsAndStakingData: IPacketsAndStakingData[];
|
||||
|
||||
currentEpochRewardsData: {
|
||||
interval: {
|
||||
reward_pool: string;
|
||||
staking_supply: string;
|
||||
staking_supply_scale_factor: string;
|
||||
epoch_reward_budget: string;
|
||||
stake_saturation_point: string;
|
||||
active_set_work_factor: string;
|
||||
interval_pool_emission: string;
|
||||
sybil_resistance: string;
|
||||
};
|
||||
rewarded_set: {
|
||||
entry_gateways: number;
|
||||
exit_gateways: number;
|
||||
mixnodes: number;
|
||||
standby: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type NodeDescription = {
|
||||
last_polled: string;
|
||||
host_information: {
|
||||
@@ -96,7 +165,7 @@ export type Location = {
|
||||
longitude?: number;
|
||||
};
|
||||
|
||||
type NodeData = {
|
||||
export type NodeData = {
|
||||
node_id: number;
|
||||
contract_node_type: string;
|
||||
description: NodeDescription;
|
||||
@@ -105,8 +174,6 @@ type NodeData = {
|
||||
location: Location;
|
||||
};
|
||||
|
||||
export default NodeData;
|
||||
|
||||
// ACCOUNT BALANCES
|
||||
|
||||
export interface IRewardDetails {
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const NYM_NODES_DESCRIBED =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/described";
|
||||
export const BONDED_NODES =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
|
||||
export const NYM_NODES =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const COSMOS_API =
|
||||
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
|
||||
export const VALIDATOR_API_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
|
||||
export const CURRENT_EPOCH =
|
||||
" https://validator.nymtech.net/api/v1/epoch/current";
|
||||
"https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const NYM_NODE_DESCRIPTION =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/described";
|
||||
export const NYM_NODE_BONDED =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
|
||||
export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account/";
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
|
||||
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
||||
export const VALIDATOR_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
||||
@@ -37,3 +19,5 @@ export const DATA_OBSERVATORY_DELEGATIONS_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/delegations";
|
||||
export const DATA_OBSERVATORY_BALANCES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/balances";
|
||||
export const OBSERVATORY_GATEWAYS_URL =
|
||||
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import CardSkeleton from "@/components/cards/Skeleton";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { CurrentEpochCard } from "@/components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "@/components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "@/components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "@/components/landingPageComponents/RewardsCard";
|
||||
import { TokenomicsCard } from "@/components/landingPageComponents/TokenomicsCard";
|
||||
import NodeTable from "@/components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { Suspense } from "react";
|
||||
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../components/headings/SectionHeading";
|
||||
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "../components/landingPageComponents/StakersNumberCard";
|
||||
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
|
||||
import NodeTable from "../components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
@@ -27,9 +25,7 @@ export default async function Home() {
|
||||
<SectionHeading title="Noise Generating Mixnet Overview" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<NoiseCard />
|
||||
</Suspense>
|
||||
<NoiseCard />
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
@@ -38,26 +34,18 @@ export default async function Home() {
|
||||
size={{ xs: 12, md: 3 }}
|
||||
>
|
||||
<Grid size={12}>
|
||||
<Suspense fallback={<CardSkeleton sx={{ width: "100%" }} />}>
|
||||
<RewardsCard />
|
||||
</Suspense>
|
||||
<RewardsCard />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Suspense fallback={<CardSkeleton sx={{ width: "100%" }} />}>
|
||||
<CurrentEpochCard />
|
||||
</Suspense>
|
||||
<CurrentEpochCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Suspense fallback={<CardSkeleton sx={{ height: "100%" }} />}>
|
||||
<NetworkStakeCard />
|
||||
</Suspense>
|
||||
<NetworkStakeCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Suspense fallback={<CardSkeleton sx={{ height: "100%" }} />}>
|
||||
<TokenomicsCard />
|
||||
</Suspense>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
@@ -65,9 +53,7 @@ export default async function Home() {
|
||||
<SectionHeading title="Nym Nodes" />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<NodeTable />
|
||||
</Suspense>
|
||||
<NodeTable />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
import type { IAccountBalancesInfo, IRewardDetails } from "@/app/api/types";
|
||||
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { IRewardDetails } from "../../app/api/types";
|
||||
import { AccountBalancesTable } from "../cards/AccountBalancesTable";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
@@ -14,8 +17,7 @@ export interface IAccontStatsRowProps {
|
||||
}
|
||||
|
||||
interface IAccountBalancesCardProps {
|
||||
accountInfo: IAccountBalancesInfo;
|
||||
nymPrice: number;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const getNymsFormated = (unyms: number) => {
|
||||
@@ -46,16 +48,59 @@ const calculateStakingRewards = (
|
||||
};
|
||||
|
||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
const { accountInfo, nymPrice } = props;
|
||||
const { address } = props;
|
||||
|
||||
const {
|
||||
data: accountInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
queryFn: () => fetchAccountBalance(address),
|
||||
enabled: !!address,
|
||||
});
|
||||
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading: isLoadingPrice,
|
||||
error: priceError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymPrice"],
|
||||
queryFn: fetchNymPrice,
|
||||
});
|
||||
|
||||
if (isLoading || isLoadingPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" height={38} />
|
||||
<Skeleton variant="text" height={380} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || priceError || !accountInfo || !nymPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const nymPriceData = nymPrice.quotes.USD.price;
|
||||
|
||||
const totalBalanceUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_value.amount),
|
||||
nymPrice,
|
||||
nymPriceData,
|
||||
);
|
||||
const spendableNYM = getNymsFormated(Number(accountInfo.balances[0].amount));
|
||||
const spendableUSD = getPriceInUSD(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
nymPrice,
|
||||
nymPriceData,
|
||||
);
|
||||
const spendableAllocation = getAllocation(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
@@ -67,7 +112,7 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
);
|
||||
const delegationsUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
nymPrice,
|
||||
nymPriceData,
|
||||
);
|
||||
const delegationsAllocation = getAllocation(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
@@ -79,7 +124,7 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
);
|
||||
const claimableUSD = getPriceInUSD(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
nymPrice,
|
||||
nymPriceData,
|
||||
);
|
||||
const claimableAllocation = getAllocation(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
|
||||
@@ -1,19 +1,49 @@
|
||||
"use client";
|
||||
import type { IAccountBalancesInfo } from "@/app/api/types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { fetchAccountBalance } from "@/app/api";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { CardQRCode } from "../qrCode/QrCode";
|
||||
|
||||
interface IAccountInfoCardProps {
|
||||
accountInfo: IAccountBalancesInfo;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
const { accountInfo } = props;
|
||||
const { address } = props;
|
||||
|
||||
const balance = Number(accountInfo.balances[0].amount) / 1000000;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
queryFn: () => fetchAccountBalance(address),
|
||||
enabled: !!address,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" height={38} />
|
||||
<Skeleton variant="rectangular" height={128} width={128} />
|
||||
<Skeleton variant="text" height={300} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const balance = Number(data.balances[0].amount) / 1000000;
|
||||
const balanceFormated = `${balance} NYM`;
|
||||
|
||||
return (
|
||||
@@ -24,7 +54,7 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
>
|
||||
<Stack gap={5}>
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<CardQRCode url={accountInfo.address} />
|
||||
<CardQRCode url={data.address} />
|
||||
</Box>
|
||||
|
||||
<ExplorerListItem
|
||||
@@ -37,8 +67,13 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">{accountInfo.address}</Typography>
|
||||
<CopyToClipboard text={accountInfo.address} />
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "85%" }}
|
||||
>
|
||||
{data.address}
|
||||
</Typography>
|
||||
<CopyToClipboard text={data.address} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import { colours } from "@/theme/colours";
|
||||
import { Button as MUIButton } from "@mui/material";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
import Link from "next/link";
|
||||
import { colours } from "../../theme/colours";
|
||||
import type { ButtonProps } from "./types";
|
||||
|
||||
export const Button = ({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
@@ -15,6 +14,7 @@ import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import * as React from "react";
|
||||
import { TABLET_WIDTH } from "../../app/constants";
|
||||
import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar";
|
||||
import { StaticProgressBar } from "../progressBars/StaticProgressBar";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ArrowUpRight from "@/components/icons/ArrowUpRight";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Image from "next/image";
|
||||
import ArrowUpRight from "../../components/icons/ArrowUpRight";
|
||||
import { Link } from "../muiLink";
|
||||
|
||||
const cardStyles = {
|
||||
@@ -40,7 +40,7 @@ const ExplorerHeroCard = ({
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
return (
|
||||
<Link href={link} sx={{ textDecoration: "none" }}>
|
||||
<Link href={link} sx={{ textDecoration: "none", height: "100%" }}>
|
||||
<Card sx={{ ...cardStyles, ...sx }} elevation={0}>
|
||||
<CardHeader
|
||||
title={
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Skeleton, Stack, type SxProps } from "@mui/material";
|
||||
import ExplorerCard from "./ExplorerCard";
|
||||
|
||||
const CardSkeleton = ({ sx }: { sx?: SxProps }) => {
|
||||
return (
|
||||
<ExplorerCard label={<Skeleton variant="text" width={200} />} sx={sx}>
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="rounded" height={75} />
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardSkeleton;
|
||||
@@ -3,7 +3,7 @@ import Flag from "react-world-flags";
|
||||
|
||||
interface ICountryFlag {
|
||||
countryCode: string;
|
||||
countryName?: string;
|
||||
countryName?: string | JSX.Element;
|
||||
}
|
||||
|
||||
const CountryFlag = ({ countryCode, countryName }: ICountryFlag) => {
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { CURRENT_EPOCH } from "@/app/api/urls";
|
||||
import {
|
||||
type EpochResponseData,
|
||||
useEpochContext,
|
||||
} from "@/providers/EpochProvider";
|
||||
import { AccessTime } from "@mui/icons-material";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { addSeconds } from "date-fns";
|
||||
import { format } from "date-fns";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
// Fetch function for the next epoch
|
||||
const fetchNextEpoch = async () => {
|
||||
const res = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch current epoch");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const dateTime = addSeconds(
|
||||
new Date(data.current_epoch_start),
|
||||
data.epoch_length.secs,
|
||||
);
|
||||
|
||||
return { data, dateTime };
|
||||
const calculateMinutesRemaining = (epochEndTime: string) => {
|
||||
const endDate = new Date(epochEndTime);
|
||||
const difference = differenceInMinutes(endDate, new Date());
|
||||
return difference;
|
||||
};
|
||||
|
||||
const NextEpochTime = () => {
|
||||
// Use React Query to fetch next epoch data
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["nextEpoch"], // Unique key for this query
|
||||
queryFn: fetchNextEpoch, // Fetch function
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is considered fresh for 60 seconds
|
||||
});
|
||||
const { data, isLoading, isError, epochStatus } = useEpochContext();
|
||||
const [minutesRemaining, setMinutesRemaining] = useState(0);
|
||||
|
||||
const updateState = useCallback((data: EpochResponseData) => {
|
||||
if (!data) return;
|
||||
const minutesRemaining = calculateMinutesRemaining(data.current_epoch_end);
|
||||
setMinutesRemaining(minutesRemaining);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateState(data);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
updateState(data);
|
||||
}, 30_000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [data, updateState]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Loading next epoch...
|
||||
Loading next mixnet epoch...
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -60,14 +57,18 @@ const NextEpochTime = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const formattedDate = format(data.dateTime, "HH:mm:ss");
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Next epoch: {formattedDate}
|
||||
</Typography>
|
||||
{epochStatus === "pending" ? (
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Waiting for next mixnet epoch to start...
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Next mixnet epoch starts in: {minutesRemaining} min
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NymLogo from "@/components/icons/NymLogo";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Divider } from "@mui/material";
|
||||
import NymLogo from "../../components/icons/NymLogo";
|
||||
import { Link } from "../../components/muiLink";
|
||||
import { Wrapper } from "../../components/wrapper";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import HeaderItem from "./HeaderItem";
|
||||
import MENU_DATA from "./menuItems";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Close as CloseIcon, Menu as MenuIcon } from "@mui/icons-material";
|
||||
import { Box, Drawer, IconButton, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { Link } from "../../components/muiLink";
|
||||
import { Wrapper } from "../../components/wrapper";
|
||||
import NymLogo from "../icons/NymLogo";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import MENU_DATA from "./menuItems";
|
||||
@@ -148,7 +148,8 @@ const MobileMenuHeader = ({
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<ConnectWallet size="small" />
|
||||
{!drawerOpen && <ConnectWallet size="small" />}
|
||||
<Box height={40} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type SxProps, TextField } from "@mui/material";
|
||||
import type { KeyboardEventHandler } from "react";
|
||||
|
||||
const Input = ({
|
||||
placeholder,
|
||||
@@ -6,6 +7,7 @@ const Input = ({
|
||||
value,
|
||||
rounded = false,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
}: {
|
||||
placeholder?: string;
|
||||
fullWidth?: boolean;
|
||||
@@ -13,6 +15,7 @@ const Input = ({
|
||||
sx?: SxProps;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLDivElement> | undefined;
|
||||
}) => {
|
||||
return (
|
||||
<TextField
|
||||
@@ -20,6 +23,7 @@ const Input = ({
|
||||
fullWidth={fullWidth}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
slotProps={{
|
||||
input: {
|
||||
sx: {
|
||||
|
||||
@@ -1,51 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import type { CurrentEpochData } from "@/app/api";
|
||||
import { CURRENT_EPOCH } from "@/app/api/urls";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
type EpochResponseData,
|
||||
useEpochContext,
|
||||
} from "@/providers/EpochProvider";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { differenceInMinutes, format } from "date-fns";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import EpochProgressBar from "../progressBars/EpochProgressBar";
|
||||
|
||||
// Fetch function
|
||||
const fetchCurrentEpoch = async (): Promise<CurrentEpochData> => {
|
||||
const response = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
const calulateProgress = (end: string) => {
|
||||
const endDate = new Date(end);
|
||||
const difference = differenceInMinutes(endDate, new Date());
|
||||
const progress = Math.max(0, 100 - (difference / 60) * 100);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch current epoch data");
|
||||
}
|
||||
return progress;
|
||||
};
|
||||
|
||||
return response.json();
|
||||
const getStartEndTime = (start: string, end: string) => {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
|
||||
const startTime = format(startDate, "HH:mm:ss");
|
||||
const endTime = format(endDate, "HH:mm:ss");
|
||||
|
||||
return { startTime, endTime };
|
||||
};
|
||||
|
||||
export const CurrentEpochCard = () => {
|
||||
// Use React Query to fetch data
|
||||
const { data, isError, isLoading } = useQuery({
|
||||
queryKey: ["currentEpoch"], // Unique query key
|
||||
queryFn: fetchCurrentEpoch, // Fetch function
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is considered fresh for 60 seconds
|
||||
});
|
||||
const { data, isError, isLoading, epochStatus } = useEpochContext();
|
||||
|
||||
const [startTime, setStartTime] = useState("");
|
||||
const [endTime, setEndTime] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const updateState = useCallback((data: NonNullable<EpochResponseData>) => {
|
||||
const { startTime, endTime } = getStartEndTime(
|
||||
data.current_epoch_start,
|
||||
data.current_epoch_end,
|
||||
);
|
||||
const progress = calulateProgress(data.current_epoch_end);
|
||||
|
||||
setStartTime(startTime);
|
||||
setEndTime(endTime);
|
||||
setProgress(progress);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
updateState(data);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
updateState(data);
|
||||
}, 30_000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, updateState]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ExplorerCard label="Current NGM epoch">Loading...</ExplorerCard>;
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Current NGM epoch">Failed to load data</ExplorerCard>
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Skeleton variant="text" height={80} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const currentEpochStart = data.current_epoch_start || "";
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
Failed to load data
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
No data available
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (epochStatus === "pending") {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography variant="body3" fontWeight="light">
|
||||
Waiting for next epoch to start...
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Current NGM epoch">
|
||||
<EpochProgressBar start={currentEpochStart} showEpoch={true} />
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<EpochProgressBar
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
progress={progress}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import {
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
} from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
"use client";
|
||||
import { fetchNoise } from "@/app/api";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "../../app/api/types";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
|
||||
export const NetworkStakeCard = async () => {
|
||||
const epochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
export const NetworkStakeCard = () => {
|
||||
const {
|
||||
data: packetsAndStaking,
|
||||
isLoading: isStakingLoading,
|
||||
isError: isStakingError,
|
||||
} = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
});
|
||||
|
||||
const packetsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const epochRewardsData: ExplorerData["currentEpochRewardsData"] =
|
||||
await epochRewards.json();
|
||||
const packetsAndStakingData: ExplorerData["packetsAndStakingData"] =
|
||||
await packetsAndStaking.json();
|
||||
|
||||
if (!epochRewardsData || !packetsAndStakingData) {
|
||||
return null;
|
||||
if (isStakingLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Current network stake">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" height={238} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const currentStake =
|
||||
Number(epochRewardsData.interval.staking_supply) / 1000000 || 0;
|
||||
if (isStakingError || !packetsAndStaking) {
|
||||
return (
|
||||
<ExplorerCard label="Current network stake">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const packetsAndStakingData: ExplorerData["packetsAndStakingData"] =
|
||||
packetsAndStaking;
|
||||
|
||||
const lastTotalStake =
|
||||
packetsAndStaking[packetsAndStaking.length - 1]?.total_stake / 1000000;
|
||||
const data = packetsAndStakingData.map((item: IPacketsAndStakingData) => {
|
||||
return {
|
||||
date_utc: item.date_utc,
|
||||
@@ -48,7 +57,7 @@ export const NetworkStakeCard = async () => {
|
||||
data,
|
||||
};
|
||||
|
||||
const title = `${formatBigNum(currentStake)} NYM`;
|
||||
const title = `${formatBigNum(lastTotalStake)} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Current network stake" sx={{ height: "100%" }}>
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import type { IPacketsAndStakingData } from "@/app/api";
|
||||
import { HARBOURMASTER_API_MIXNODES_STATS } from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
"use client";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { IPacketsAndStakingData } from "../../app/api/types";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
import { UpDownPriceIndicator } from "../price/UpDownPriceIndicator";
|
||||
|
||||
export const NoiseCard = async () => {
|
||||
const response = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
import { fetchNoise } from "@/app/api";
|
||||
|
||||
export const NoiseCard = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
});
|
||||
|
||||
const data: IPacketsAndStakingData[] = await response.json();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Noise generated last 24h">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" height={238} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Noise generated last 24h">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const todaysData = data[data.length - 1];
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const RewardsCard = async () => {
|
||||
return (
|
||||
<ExplorerCard label="Operator rewards this month">
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{`${formatBigNum(10_000_111)} NYM`}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const RewardsCard = () => {
|
||||
const {
|
||||
data: observatoryNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["observatoryNodes"],
|
||||
queryFn: () => fetchObservatoryNodes(),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Active stakers number">
|
||||
<Skeleton variant="text" height={90} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !observatoryNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Active stakers number">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
const getActiveStakersNumber = (nodes: IObservatoryNode[]): number => {
|
||||
return nodes.reduce(
|
||||
(sum, node) => sum + node.rewarding_details.unique_delegations,
|
||||
0,
|
||||
);
|
||||
};
|
||||
const allStakers = getActiveStakersNumber(observatoryNodes);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Active stakers number">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
{allStakers}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,71 @@
|
||||
import type { NymTokenomics } from "@/app/api/types";
|
||||
import { NYM_PRICES_API } from "@/app/api/urls";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
"use client";
|
||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ExplorerData, NymTokenomics } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { TitlePrice } from "../price/TitlePrice";
|
||||
|
||||
export const TokenomicsCard = async () => {
|
||||
const nymPrice = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
export const TokenomicsCard = () => {
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymPrice"],
|
||||
queryFn: fetchNymPrice,
|
||||
});
|
||||
|
||||
const nymPriceData: NymTokenomics = await nymPrice.json();
|
||||
const {
|
||||
data: epochRewards,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
});
|
||||
|
||||
const {
|
||||
data: packetsAndStaking,
|
||||
isLoading: isStakingLoading,
|
||||
isError: isStakingError,
|
||||
} = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
});
|
||||
|
||||
if (isLoading || isEpochLoading || isStakingLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Tokenomics overview">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" height={238} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isStakingError ||
|
||||
isEpochError ||
|
||||
isError ||
|
||||
!nymPrice ||
|
||||
!epochRewards ||
|
||||
!packetsAndStaking
|
||||
) {
|
||||
return (
|
||||
<ExplorerCard label="Tokenomics overview">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to load tokenomics overview.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={80} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const nymPriceData: NymTokenomics = nymPrice;
|
||||
const nymPriceDataFormated = Number(nymPriceData.quotes.USD.price.toFixed(2));
|
||||
|
||||
const titlePrice = {
|
||||
@@ -25,11 +75,35 @@ export const TokenomicsCard = async () => {
|
||||
// numberWentUp: true,
|
||||
// },
|
||||
};
|
||||
const marketCap = nymPriceData.quotes.USD.market_cap;
|
||||
const volume24H = nymPriceData.quotes.USD.volume_24h.toFixed(2);
|
||||
const marketCap = formatBigNum(nymPriceData.quotes.USD.market_cap);
|
||||
const volume24H = formatBigNum(nymPriceData.quotes.USD.volume_24h);
|
||||
|
||||
const epochRewardsData: ExplorerData["currentEpochRewardsData"] =
|
||||
epochRewards;
|
||||
const packetsAndStakingData: ExplorerData["packetsAndStakingData"] =
|
||||
packetsAndStaking;
|
||||
|
||||
function calculateTVL(
|
||||
epochRewards: ExplorerData["currentEpochRewardsData"],
|
||||
nymPriceData: NymTokenomics,
|
||||
packetsAndStaking: ExplorerData["packetsAndStakingData"],
|
||||
): number {
|
||||
const lastTotalStake =
|
||||
packetsAndStaking[packetsAndStaking.length - 1]?.total_stake || 0;
|
||||
return (
|
||||
(Number.parseFloat(epochRewards.interval.reward_pool) / 1000000 +
|
||||
lastTotalStake / 1000000) *
|
||||
nymPriceData.quotes.USD.price
|
||||
);
|
||||
}
|
||||
const TVL = formatBigNum(
|
||||
calculateTVL(epochRewardsData, nymPrice, packetsAndStakingData),
|
||||
);
|
||||
|
||||
const dataRows = [
|
||||
{ key: "Market cap", value: `$ ${marketCap}` },
|
||||
{ key: "24H VOL", value: `$ ${volume24H}` },
|
||||
{ key: "TVL", value: `$ ${TVL}` },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -66,8 +66,8 @@ export const LineChart = ({
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 30,
|
||||
right: 12,
|
||||
left: 36,
|
||||
right: 18,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
@@ -106,7 +106,7 @@ export const LineChart = ({
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
tickValues: 8,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
|
||||
@@ -12,7 +12,7 @@ const modalStyle: SxProps = {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 500,
|
||||
width: 300,
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
borderRadius: "16px",
|
||||
|
||||
@@ -22,7 +22,7 @@ const InfoModal = (props: InfoModalProps) => {
|
||||
}
|
||||
|
||||
const { open, onClose, title, message, tx, Action } = props;
|
||||
const mintscanURL = tx ? `https://www.mintscan.io/nyx/tx/${tx}` : "/";
|
||||
const pingURL = tx ? `https://www.ping.pub/nyx/tx/${tx}` : "/";
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
@@ -41,7 +41,7 @@ const InfoModal = (props: InfoModalProps) => {
|
||||
<Typography variant="h3">{title}</Typography>
|
||||
<Typography variant="body3">{message}</Typography>
|
||||
{tx && (
|
||||
<Link href={mintscanURL}>
|
||||
<Link href={pingURL} rel="noopener noreferrer" target="_blank">
|
||||
<Typography variant="h5">Block explorer link</Typography>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Cross from "@/components/icons/Cross";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Cross from "../../components/icons/Cross";
|
||||
|
||||
type SimpleModalPropsClosed = {
|
||||
open: false;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import type React from "react";
|
||||
import { TABLET_WIDTH } from "../../app/constants";
|
||||
|
||||
export const modalStyle = (width: number | string = 600) => ({
|
||||
position: "absolute",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Imports
|
||||
import { Link as MuiLink, type LinkProps as MuiLinkProps } from "@mui/material";
|
||||
import NextLink, { type LinkProps as NextLinkProps } from "next/link";
|
||||
// Imports
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface CustomLinkProps extends NextLinkProps, Omit<MuiLinkProps, "href"> {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Box, Button, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
@@ -12,9 +17,13 @@ import {
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite } from "../favorite/Favorite";
|
||||
import Loading from "../loading";
|
||||
// import Loading from "../loading";
|
||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||
import StakeModal from "../staking/StakeModal";
|
||||
import { fee } from "../staking/schemas";
|
||||
@@ -26,16 +35,42 @@ const ColumnHeading = ({
|
||||
}: {
|
||||
children: string | React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
return (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }} variant="h5">
|
||||
{children}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "80px" : "unset",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "baseline",
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
py: 2,
|
||||
textAlign: "center",
|
||||
whiteSpace: isMobile ? "normal" : "unset", // Ensure text can wrap
|
||||
wordWrap: isMobile ? "break-word" : "unset", // Break long words
|
||||
overflowWrap: isMobile ? "break-word" : "unset", // Ensure text breaks inside the cell
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
variant={isMobile ? "caption" : "h5"}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const router = useRouter();
|
||||
const { nymClient } = useNymClient();
|
||||
const queryClient = useQueryClient();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
open: false,
|
||||
@@ -48,6 +83,10 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const handleRefetch = useCallback(async () => {
|
||||
await queryClient.invalidateQueries();
|
||||
}, [queryClient]);
|
||||
|
||||
const handleStakeOnNode = useCallback(
|
||||
async ({ nodeId, amount }: { nodeId: number; amount: string }) => {
|
||||
const amountToDelegate = (Number(amount) * 1_000_000).toString();
|
||||
@@ -69,8 +108,12 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
message: "This operation can take up to one hour to process",
|
||||
tx: tx?.transactionHash,
|
||||
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
onClose: async () => {
|
||||
await handleRefetch();
|
||||
setInfoModalProps({ open: false });
|
||||
},
|
||||
});
|
||||
await handleRefetch();
|
||||
} catch (e) {
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : "An error occurred while staking";
|
||||
@@ -85,7 +128,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[nymClient],
|
||||
[nymClient, handleRefetch],
|
||||
);
|
||||
|
||||
const handleOnSelectStake = useCallback(
|
||||
@@ -116,6 +159,12 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
[isWalletConnected],
|
||||
);
|
||||
// get full country name
|
||||
const countryName = useCallback((countryCode: string) => {
|
||||
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
|
||||
return <span>{regionNames.of(countryCode)}</span>;
|
||||
}, []);
|
||||
|
||||
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
||||
() => [
|
||||
@@ -131,23 +180,36 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "node",
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
accessorKey: "identity_key",
|
||||
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: "Quality of Service",
|
||||
header: "Qlt of Service",
|
||||
align: "center",
|
||||
accessorKey: "qualityOfService",
|
||||
Header: <ColumnHeading>Quality of Service</ColumnHeading>,
|
||||
size: 100,
|
||||
|
||||
Header: <ColumnHeading>Qlt of Service</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.qualityOfService}%
|
||||
@@ -158,17 +220,16 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
id: "location",
|
||||
header: "Location",
|
||||
accessorKey: "countryName",
|
||||
size: 160,
|
||||
Header: <ColumnHeading>Location</ColumnHeading>,
|
||||
Cell: ({ row }) =>
|
||||
row.original.countryCode && row.original.countryName ? (
|
||||
<Tooltip title={row.original.countryName}>
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={row.original.countryCode || ""}
|
||||
countryName={row.original.countryCode || ""}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box width="100%">
|
||||
<CountryFlag
|
||||
countryCode={row.original.countryCode || ""}
|
||||
countryName={countryName(row.original.countryName) || ""}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
@@ -199,6 +260,8 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
id: "Action",
|
||||
header: "Action",
|
||||
accessorKey: "Action",
|
||||
size: 120,
|
||||
|
||||
Header: <ColumnHeading>Action</ColumnHeading>,
|
||||
hidden: !isWalletConnected,
|
||||
enableColumnFilter: false,
|
||||
@@ -221,6 +284,8 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
enableColumnFilter: false,
|
||||
header: "Favorite",
|
||||
accessorKey: "Favorite",
|
||||
size: 110,
|
||||
|
||||
Header: (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<ColumnHeading>Favorite</ColumnHeading>
|
||||
@@ -241,7 +306,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
Cell: ({ row }) => <Favorite address={row.original.owner} />,
|
||||
},
|
||||
],
|
||||
[isWalletConnected, handleOnSelectStake, favorites],
|
||||
[isWalletConnected, handleOnSelectStake, favorites, countryName],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
@@ -264,7 +329,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
shape: "circular",
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: { right: ["Action", "Favorite"] },
|
||||
columnPinning: isMobile ? {} : { right: ["Action", "Favorite"] }, // No pinning on mobile
|
||||
},
|
||||
muiColumnActionsButtonProps: {
|
||||
sx: {
|
||||
@@ -280,9 +345,17 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
bgcolor: "background.paper",
|
||||
},
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
alignItems: "center",
|
||||
},
|
||||
},
|
||||
|
||||
muiTableBodyCellProps: {
|
||||
sx: {
|
||||
border: "none",
|
||||
whiteSpace: "unset", // Allow text wrapping in body cells
|
||||
wordBreak: "break-word", // Ensure long text breaks correctly
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
@@ -301,9 +374,11 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Loading />}
|
||||
|
||||
<StakeModal
|
||||
nodeId={selectedNodeForStaking?.nodeId}
|
||||
identityKey={selectedNodeForStaking?.identityKey}
|
||||
@@ -312,6 +387,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
/>
|
||||
|
||||
<InfoModal {...infoModalProps} />
|
||||
|
||||
<MaterialReactTable table={table} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import getNymNodes from "@/actions/getNymNodes";
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { CURRENT_EPOCH_REWARDS } from "@/app/api/urls";
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import NodeTable from "./NodeTable";
|
||||
|
||||
// Fetch function for epoch rewards
|
||||
const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch epoch rewards");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Utility function to calculate node saturation point
|
||||
function getNodeSaturationPoint(
|
||||
totalStake: number,
|
||||
@@ -52,8 +34,12 @@ const mappedNymNodes = (
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
return {
|
||||
name: node.self_description.moniker,
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
@@ -78,8 +64,6 @@ const NodeTableWithAction = () => {
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
});
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
@@ -89,23 +73,41 @@ const NodeTableWithAction = () => {
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: getNymNodes,
|
||||
staleTime: 60000,
|
||||
refetchInterval: 60000,
|
||||
queryFn: fetchObservatoryNodes,
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
return <div>Error loading data. Please try again later.</div>;
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Error loading data. Please try again later.
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const data = mappedNymNodes(nymNodes, epochRewardsData);
|
||||
|
||||
if (!epochRewardsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
|
||||
return <NodeTable nodes={data} />;
|
||||
};
|
||||
|
||||
@@ -1,70 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { fetchNodeInfo } from "../../app/api";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface IBasicInfoCardProps {
|
||||
id: number; // Node ID
|
||||
id: number;
|
||||
}
|
||||
|
||||
// Fetch function to get the node data
|
||||
const fetchNodeInfo = async (id: number): Promise<IObservatoryNode | null> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const observatoryNymNodes: IObservatoryNode[] = await response.json();
|
||||
|
||||
return observatoryNymNodes.find((node) => node.node_id === id) || null;
|
||||
};
|
||||
|
||||
export const BasicInfoCard = ({ id }: IBasicInfoCardProps) => {
|
||||
// Use React Query to fetch the node info
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id], // Unique query key based on the node ID
|
||||
queryFn: () => fetchNodeInfo(id), // Fetch function
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is considered fresh for 60 seconds
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
});
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography>Loading...</Typography>
|
||||
<Skeleton variant="text" height={90} />
|
||||
<Skeleton variant="text" height={90} />
|
||||
<Skeleton variant="text" height={70} />
|
||||
<Skeleton variant="text" height={70} />
|
||||
<Skeleton variant="text" height={70} />
|
||||
<Skeleton variant="text" height={70} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (isError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography>Failed to load node information.</Typography>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Derived data from nodeInfo
|
||||
const timeBonded = format(
|
||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
@@ -92,7 +73,10 @@ export const BasicInfoCard = ({ id }: IBasicInfoCardProps) => {
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
|
||||
>
|
||||
{nodeInfo.bonding_address}
|
||||
</Typography>
|
||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||
@@ -110,7 +94,12 @@ export const BasicInfoCard = ({ id }: IBasicInfoCardProps) => {
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">{nodeInfo.identity_key}</Typography>
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "85%" }}
|
||||
>
|
||||
{nodeInfo.identity_key}
|
||||
</Typography>
|
||||
<CopyToClipboard text={nodeInfo.identity_key} />
|
||||
</Stack>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { NodeRewardDetails } from "@/app/api/types";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import type { NodeRewardDetails } from "../../app/api/types";
|
||||
|
||||
const ColumnHeading = ({
|
||||
children,
|
||||
@@ -62,6 +62,11 @@ const DelegationsTable = ({
|
||||
header: "Amount",
|
||||
accessorKey: "amount",
|
||||
Header: <ColumnHeading>Amount</ColumnHeading>,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const stakeA = Number.parseFloat(rowA.original.amount.amount);
|
||||
const stakeB = Number.parseFloat(rowB.original.amount.amount);
|
||||
return stakeA - stakeB;
|
||||
},
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{getNymsFormated(row.original.amount.amount)} NYM
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { NodeRewardDetails } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { fetchNodeDelegations } from "@/app/api";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import DelegationsTable from "./DelegationsTable";
|
||||
@@ -10,30 +10,7 @@ interface NodeDelegationsCardProps {
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
// Fetch delegations dynamically based on ID
|
||||
const fetchNodeDelegations = async (
|
||||
id: number,
|
||||
): Promise<NodeRewardDetails[]> => {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch delegations");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const NodeDelegationsCard = ({ id }: NodeDelegationsCardProps) => {
|
||||
// Use React Query to fetch delegations
|
||||
const {
|
||||
data: delegations = [],
|
||||
isLoading,
|
||||
@@ -41,17 +18,32 @@ const NodeDelegationsCard = ({ id }: NodeDelegationsCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["nodeDelegations", id],
|
||||
queryFn: () => fetchNodeDelegations(id),
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load delegations. Please try again later.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
{isLoading && <div>Loading delegations...</div>}
|
||||
{isError && (
|
||||
<div>Failed to load delegations. Please try again later.</div>
|
||||
)}
|
||||
{!isLoading && !isError && <DelegationsTable delegations={delegations} />}
|
||||
<DelegationsTable delegations={delegations} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import {
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
} from "@/app/api/urls";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchEpochRewards, fetchNodeInfo } from "../../app/api";
|
||||
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -14,42 +11,7 @@ interface INodeMetricsCardProps {
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
// Fetch functions
|
||||
const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch epoch rewards");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const fetchNodeInfo = async (id: number): Promise<IObservatoryNode | null> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
return nodes.find((node) => node.node_id === id) || null;
|
||||
};
|
||||
|
||||
export const NodeMetricsCard = ({ id }: INodeMetricsCardProps) => {
|
||||
// Fetch epoch rewards
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
@@ -57,8 +19,6 @@ export const NodeMetricsCard = ({ id }: INodeMetricsCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
// Fetch node information
|
||||
@@ -69,14 +29,15 @@ export const NodeMetricsCard = ({ id }: INodeMetricsCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (isEpochLoading || isNodeLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<div>Loading...</div>
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +45,9 @@ export const NodeMetricsCard = ({ id }: INodeMetricsCardProps) => {
|
||||
if (isEpochError || isNodeError || !nodeInfo || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<div>Failed to load data</div>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RandomAvatar } from "react-random-avatars";
|
||||
import { fetchNodeInfo } from "../../app/api";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite } from "../favorite/Favorite";
|
||||
@@ -21,23 +22,6 @@ interface INodeProfileCardProps {
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
// Fetch node info
|
||||
const fetchNodeInfo = async (id: number) => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const nodes = await response.json();
|
||||
return nodes.find((node: { node_id: number }) => node.node_id === id) || null;
|
||||
};
|
||||
|
||||
export const NodeProfileCard = ({ id }: INodeProfileCardProps) => {
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
const { nymClient } = useNymClient();
|
||||
@@ -58,8 +42,6 @@ export const NodeProfileCard = ({ id }: INodeProfileCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const handleStakeOnNode = async ({
|
||||
@@ -135,7 +117,9 @@ export const NodeProfileCard = ({ id }: INodeProfileCardProps) => {
|
||||
if (isNodeLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<div>Loading...</div>
|
||||
<Skeleton variant="rectangular" height={80} width={80} />
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" height={200} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -143,10 +127,26 @@ export const NodeProfileCard = ({ id }: INodeProfileCardProps) => {
|
||||
if (isNodeError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<div>Failed to load node information.</div>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
const cleanDescription = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.details,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
// get full country name
|
||||
const countryName = (countryCode: string) => {
|
||||
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
|
||||
return <span>{regionNames.of(countryCode)}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
@@ -160,20 +160,25 @@ export const NodeProfileCard = ({ id }: INodeProfileCardProps) => {
|
||||
mb={1}
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{nodeInfo?.self_description.moniker || "Moniker"}
|
||||
{cleanMoniker || "Moniker"}
|
||||
</Typography>
|
||||
{nodeInfo.description.auxiliary_details.location && (
|
||||
<Box display={"flex"} gap={1}>
|
||||
<Typography variant="h6">Location:</Typography>
|
||||
<CountryFlag
|
||||
countryCode={nodeInfo.description.auxiliary_details.location}
|
||||
countryName={nodeInfo.description.auxiliary_details.location}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={nodeInfo.description.auxiliary_details.location}
|
||||
countryName={countryName(
|
||||
nodeInfo.description.auxiliary_details.location,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{nodeInfo && (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }} mt={2}>
|
||||
{nodeInfo.self_description.details}
|
||||
{cleanDescription}
|
||||
</Typography>
|
||||
)}
|
||||
<Box mt={3} display={"flex"} gap={1}>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import type { IObservatoryNode, RewardingDetails } from "@/app/api/types";
|
||||
import {
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
} from "@/app/api/urls";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchEpochRewards, fetchNodeInfo } from "../../app/api";
|
||||
import type { RewardingDetails } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -14,40 +11,6 @@ interface INodeRewardsCardProps {
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
// Fetch functions
|
||||
const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch epoch rewards");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const fetchNodeInfo = async (id: number): Promise<IObservatoryNode | null> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
return nodes.find((node) => node.node_id === id) || null;
|
||||
};
|
||||
|
||||
export const NodeRewardsCard = ({ id }: INodeRewardsCardProps) => {
|
||||
// Fetch epoch rewards
|
||||
const {
|
||||
@@ -57,8 +20,6 @@ export const NodeRewardsCard = ({ id }: INodeRewardsCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
// Fetch node information
|
||||
@@ -69,8 +30,6 @@ export const NodeRewardsCard = ({ id }: INodeRewardsCardProps) => {
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (isEpochLoading || isNodeLoading) {
|
||||
@@ -79,7 +38,10 @@ export const NodeRewardsCard = ({ id }: INodeRewardsCardProps) => {
|
||||
label="Node rewards (last epoch/hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<div>Loading...</div>
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
<Skeleton variant="text" height={50} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -90,7 +52,9 @@ export const NodeRewardsCard = ({ id }: INodeRewardsCardProps) => {
|
||||
label="Node rewards (last epoch/hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<div>Failed to load data</div>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
GatewayStatus,
|
||||
IObservatoryNode,
|
||||
LastProbeResult,
|
||||
NodeDescription,
|
||||
} from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { Chip, Stack } from "@mui/material";
|
||||
import { Chip, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchGatewayStatus, fetchNodeInfo } from "../../app/api";
|
||||
import type { LastProbeResult, NodeDescription } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import StarRating from "../starRating/StarRating";
|
||||
@@ -28,38 +23,6 @@ const roleMapping: Record<DeclaredRoleKey, RoleString> = {
|
||||
mixnode: "Mix Node",
|
||||
};
|
||||
|
||||
// Fetch node data based on ID
|
||||
const fetchNodeInfo = async (id: number): Promise<IObservatoryNode | null> => {
|
||||
const response = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
return nodes.find((node) => node.node_id === id) || null;
|
||||
};
|
||||
|
||||
// Fetch gateway status based on identity key
|
||||
const fetchGatewayStatus = async (
|
||||
identityKey: string,
|
||||
): Promise<GatewayStatus | null> => {
|
||||
const response = await fetch(
|
||||
`https://mainnet-node-status-api.nymtech.cc/v2/gateways/${identityKey}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch gateway status");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getNodeRoles = (
|
||||
declaredRoles: NodeDescriptionNotNull["declared_role"],
|
||||
): RoleString[] => {
|
||||
@@ -180,39 +143,50 @@ export const QualityIndicatorsCard = ({ id }: IQualityIndicatorsCardProps) => {
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading: isNodeLoading,
|
||||
isError: isNodeError,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
// Fetch gateway status if nodeInfo is available
|
||||
// Extract node roles once `nodeInfo` is available
|
||||
const nodeRoles = nodeInfo
|
||||
? getNodeRoles(nodeInfo.description.declared_role)
|
||||
: [];
|
||||
|
||||
// Define whether to fetch gateway status
|
||||
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
|
||||
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
|
||||
);
|
||||
|
||||
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
|
||||
const { data: gatewayStatus } = useQuery({
|
||||
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
|
||||
queryFn: () => fetchGatewayStatus(nodeInfo?.identity_key),
|
||||
enabled: !!nodeInfo?.identity_key, // Only fetch if identity key is available
|
||||
queryFn: () => fetchGatewayStatus(nodeInfo?.identity_key || ""),
|
||||
enabled: !!nodeInfo?.identity_key && shouldFetchGatewayStatus, // ✅ Only fetch if needed
|
||||
});
|
||||
|
||||
if (isNodeLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Quality indicators">
|
||||
<div>Loading...</div>
|
||||
<Skeleton variant="text" height={70} />
|
||||
<Skeleton variant="text" height={70} />
|
||||
<Skeleton variant="text" height={300} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNodeError || !nodeInfo) {
|
||||
if (isError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Quality indicators">
|
||||
<div>Failed to load node data.</div>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const nodeRoles = getNodeRoles(nodeInfo.description.declared_role);
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
<Stack key={role} direction="row" gap={1}>
|
||||
<Chip key={role} label={role} size="small" />
|
||||
@@ -254,7 +228,7 @@ export const QualityIndicatorsCard = ({ id }: IQualityIndicatorsCardProps) => {
|
||||
label="Quality of service"
|
||||
value={<StarRating value={qualityOfServiceStars} />}
|
||||
/>
|
||||
{!nodeIsMixNodeOnly && (
|
||||
{!nodeIsMixNodeOnly && gatewayStatus && (
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
@@ -262,7 +236,7 @@ export const QualityIndicatorsCard = ({ id }: IQualityIndicatorsCardProps) => {
|
||||
value={<StarRating value={configScoreStars} />}
|
||||
/>
|
||||
)}
|
||||
{!nodeIsMixNodeOnly && (
|
||||
{!nodeIsMixNodeOnly && gatewayStatus && (
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { colours } from "@/theme/colours";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import type React from "react";
|
||||
import type { ReactElement } from "react";
|
||||
import { colours } from "../../theme/colours";
|
||||
|
||||
export interface IUpDownPriceIndicatorProps {
|
||||
percentage: number;
|
||||
|
||||
@@ -1,73 +1,31 @@
|
||||
"use client";
|
||||
import { Stack } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import { addHours, format } from "date-fns";
|
||||
import React from "react";
|
||||
import ListItem from "../list/ListItem";
|
||||
import ProgressBar from "./ProgressBar";
|
||||
|
||||
export interface IDynamicProgressBarProps {
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const EpochProgressBar = ({ start, showEpoch }: IDynamicProgressBarProps) => {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
const startDate = new Date(start);
|
||||
const endDate = addHours(new Date(start), 1);
|
||||
const startTime = format(startDate, "HH:mm dd-MM-yyyy");
|
||||
const endTime = format(endDate, "HH:mm dd-MM-yyyy");
|
||||
|
||||
React.useEffect(() => {
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate start timestamp
|
||||
if (Number.isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to calculate progress
|
||||
const calculateProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime < startTime) {
|
||||
return 0;
|
||||
}
|
||||
if (currentTime >= endTime) {
|
||||
return 100;
|
||||
}
|
||||
const elapsed = currentTime - startTime;
|
||||
const total = endTime - startTime;
|
||||
return (elapsed / total) * 100;
|
||||
};
|
||||
|
||||
// Set initial progress and start timer
|
||||
setProgress(calculateProgress());
|
||||
const timer = setInterval(() => {
|
||||
setProgress(calculateProgress());
|
||||
}, 60000); // Update every minute (60000 milliseconds)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
const EpochProgressBar = ({
|
||||
startTime,
|
||||
endTime,
|
||||
progress,
|
||||
}: IDynamicProgressBarProps) => {
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<ProgressBar value={progress} color="secondary" />
|
||||
|
||||
{showEpoch && (
|
||||
<Box mt={3}>
|
||||
<Stack gap={0}>
|
||||
<ListItem row label="START" value={startTime} />
|
||||
<ListItem row label="END" value={endTime} />
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
<Box mt={3}>
|
||||
<Stack gap={0}>
|
||||
<ListItem row label="START" value={startTime} />
|
||||
<ListItem row label="END" value={endTime} />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SimpleModal from "@/components/modal/SimpleModal";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
import SimpleModal from "../../components/modal/SimpleModal";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
|
||||
const RedeemRewardsModal = ({
|
||||
totalRewardsAmount,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_NODE_BONDED } from "@/app/api/urls";
|
||||
import type { NodeData } from "@/app/api/types";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { NYM_NODE_BONDED } from "../../app/api/urls";
|
||||
import Input from "../input/Input";
|
||||
|
||||
const NodeAndAddressSearch = () => {
|
||||
@@ -32,11 +32,19 @@ const NodeAndAddressSearch = () => {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
setErrorText("Such Nym address doesn't exist");
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setErrorText("Such Nym address doesn't exist");
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -55,12 +63,16 @@ const NodeAndAddressSearch = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setErrorText("Such Nym Node identity key doesn't exist");
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorText("An unexpected error occurred. Please try again.");
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
);
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false); // Stop loading
|
||||
}
|
||||
};
|
||||
@@ -73,6 +85,11 @@ const NodeAndAddressSearch = () => {
|
||||
fullWidth
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
rounded
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { ObservatoryBalance } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_BALANCES_URL } from "@/app/api/urls";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchOriginalStake } from "../../app/api";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
// Fetch function to get the original stake
|
||||
const fetchOriginalStake = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the delegated amount
|
||||
return Number(balances.delegated.amount);
|
||||
};
|
||||
|
||||
const OriginalStakeCard = () => {
|
||||
const { address } = useNymClient();
|
||||
|
||||
@@ -40,8 +19,6 @@ const OriginalStakeCard = () => {
|
||||
queryKey: ["originalStake", address],
|
||||
queryFn: () => fetchOriginalStake(address || ""),
|
||||
enabled: !!address, // Only fetch if address exists
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -51,7 +28,7 @@ const OriginalStakeCard = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
<Skeleton variant="text" />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +36,7 @@ const OriginalStakeCard = () => {
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography variant="body2" color="error">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load original stake.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { Grid2 } from "@mui/material";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import OriginalStakeCard from "./OriginalStakeCard";
|
||||
import TotalRewardsCard from "./TotalRewardsCard";
|
||||
import TotalStakeCard from "./TotalStakeCard";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import SimpleModal from "@/components/modal/SimpleModal";
|
||||
import useGetWalletBalance from "@/hooks/useGetWalletBalance";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
import { CurrencyFormField } from "@nymproject/react/currency/CurrencyFormField.js";
|
||||
import { IdentityKeyFormField } from "@nymproject/react/mixnodes/IdentityKeyFormField.js";
|
||||
import type { DecCoin } from "@nymproject/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import SimpleModal from "../../components/modal/SimpleModal";
|
||||
import useGetWalletBalance from "../../hooks/useGetWalletBalance";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import stakingSchema, { MIN_AMOUNT_TO_DELEGATE } from "./schemas";
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { Box, Button, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
@@ -14,7 +21,12 @@ import {
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import usePendingEvents, {
|
||||
type PendingEvent,
|
||||
} from "../../../src/hooks/useGetPendingEvents";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite } from "../favorite/Favorite";
|
||||
import Loading from "../loading";
|
||||
@@ -29,6 +41,7 @@ import { fee } from "./schemas";
|
||||
type DelegationWithNodeDetails = {
|
||||
node: MappedNymNode | undefined;
|
||||
delegation: Delegation;
|
||||
pendingEvent?: PendingEvent;
|
||||
};
|
||||
|
||||
const ColumnHeading = ({
|
||||
@@ -36,19 +49,42 @@ const ColumnHeading = ({
|
||||
}: {
|
||||
children: string | React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
return (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }} variant="h5">
|
||||
{children}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "80px" : "unset",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "baseline",
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
py: 2,
|
||||
textAlign: "center",
|
||||
whiteSpace: isMobile ? "normal" : "unset", // Ensure text can wrap
|
||||
wordWrap: isMobile ? "break-word" : "unset", // Break long words
|
||||
overflowWrap: isMobile ? "break-word" : "unset", // Ensure text breaks inside the cell
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
variant={isMobile ? "caption" : "h5"}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const { nymClient, address } = useNymClient();
|
||||
const { nymClient, address, nymQueryClient } = useNymClient();
|
||||
const [delegations, setDelegations] = useState<DelegationWithNodeDetails[]>(
|
||||
[],
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDataLoading, setIsLoading] = useState(false);
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
open: false,
|
||||
});
|
||||
@@ -58,11 +94,23 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
}>();
|
||||
const [favorites] = useLocalStorage<string[]>("nym-node-favorites", []);
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
const { data: pendingEvents } = usePendingEvents(nymQueryClient, address);
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Custom Hook for fetching pending events
|
||||
|
||||
const handleRefetch = useCallback(async () => {
|
||||
await queryClient.invalidateQueries();
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nymClient || !address) return;
|
||||
if (!nymClient || !address || !nymQueryClient) return;
|
||||
|
||||
// Fetch staking data
|
||||
const fetchDelegations = async () => {
|
||||
@@ -72,16 +120,68 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
return data.delegations;
|
||||
};
|
||||
|
||||
// Combine delegations with node details
|
||||
const combineDelegationsWithNode = (delegations: Delegation[]) => {
|
||||
// Combine delegations with node details and pending events
|
||||
const combineDelegationsWithNodeAndPendingEvents = (
|
||||
delegations: Delegation[],
|
||||
nodes: MappedNymNode[],
|
||||
pendingEvents: PendingEvent[] | undefined,
|
||||
) => {
|
||||
// Combine delegations with node details
|
||||
const delegationsWithNodeDetails = delegations.map((delegation) => {
|
||||
const node = nodes.find((node) => node.nodeId === delegation.node_id);
|
||||
const pendingEvent = pendingEvents?.find(
|
||||
(event) => event?.mixId === delegation.node_id,
|
||||
);
|
||||
console.log("node,delegation,pendingEvent,:>> ", {
|
||||
node,
|
||||
delegation,
|
||||
pendingEvent,
|
||||
});
|
||||
return {
|
||||
node,
|
||||
delegation,
|
||||
pendingEvent,
|
||||
};
|
||||
});
|
||||
|
||||
// Add pending events that are not in the delegations list
|
||||
if (pendingEvents) {
|
||||
for (const e of pendingEvents) {
|
||||
if (
|
||||
e &&
|
||||
!delegationsWithNodeDetails.find(
|
||||
(item) =>
|
||||
item.node?.nodeId === e.mixId ||
|
||||
item.delegation.node_id === e.mixId,
|
||||
)
|
||||
) {
|
||||
delegationsWithNodeDetails.push({
|
||||
node: {
|
||||
name: "-",
|
||||
nodeId: e.mixId,
|
||||
identity_key: "-",
|
||||
countryCode: null,
|
||||
countryName: null,
|
||||
profitMarginPercentage: 0,
|
||||
owner: "-",
|
||||
stakeSaturation: 0,
|
||||
},
|
||||
pendingEvent: e,
|
||||
delegation: {
|
||||
amount: {
|
||||
amount: e.amount?.amount || "0",
|
||||
denom: "unym",
|
||||
},
|
||||
cumulative_reward_ratio: "0",
|
||||
height: 0,
|
||||
node_id: e.mixId,
|
||||
owner: "-",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return delegationsWithNodeDetails;
|
||||
};
|
||||
|
||||
@@ -89,12 +189,17 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const fetchAndMapDelegations = async () => {
|
||||
const delegations = await fetchDelegations();
|
||||
const delegationsWithNodeDetails =
|
||||
combineDelegationsWithNode(delegations);
|
||||
combineDelegationsWithNodeAndPendingEvents(
|
||||
delegations,
|
||||
nodes,
|
||||
pendingEvents,
|
||||
);
|
||||
|
||||
setDelegations(delegationsWithNodeDetails);
|
||||
};
|
||||
|
||||
fetchAndMapDelegations();
|
||||
}, [address, nodes, nymClient]);
|
||||
}, [address, nodes, nymClient, nymQueryClient, pendingEvents]);
|
||||
|
||||
const handleStakeOnNode = useCallback(
|
||||
async ({ nodeId, amount }: { nodeId: number; amount: string }) => {
|
||||
@@ -111,13 +216,17 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
uNymFunds,
|
||||
);
|
||||
setSelectedNodeForStaking(undefined);
|
||||
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Success",
|
||||
message: "This operation can take up to one hour to process",
|
||||
tx: tx?.transactionHash,
|
||||
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
onClose: async () => {
|
||||
await handleRefetch();
|
||||
setInfoModalProps({ open: false });
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage =
|
||||
@@ -133,7 +242,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[nymClient],
|
||||
[nymClient, handleRefetch],
|
||||
);
|
||||
|
||||
const handleOnSelectStake = useCallback(
|
||||
@@ -183,6 +292,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
`Explorer V2: Unstaking node ${nodeId}`,
|
||||
);
|
||||
setIsLoading(false);
|
||||
await handleRefetch();
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Success",
|
||||
@@ -204,7 +314,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[address, nymClient],
|
||||
[address, nymClient, handleRefetch],
|
||||
);
|
||||
|
||||
const handleActionSelect = useCallback(
|
||||
@@ -223,6 +333,30 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
[handleUnstake, handleOnSelectStake],
|
||||
);
|
||||
|
||||
const getTooltipTitle = useCallback(
|
||||
(pending: PendingEvent) => {
|
||||
if (pending?.kind === "undelegate") {
|
||||
return "You have an undelegation pending";
|
||||
}
|
||||
|
||||
if (pending?.kind === "delegate") {
|
||||
return `You have a delegation pending worth ${formatBigNum(
|
||||
+pending.amount.amount / 1_000_000,
|
||||
)} NYM`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[], // Add dependencies if necessary
|
||||
);
|
||||
|
||||
// get full country name
|
||||
const countryName = useCallback((countryCode: string) => {
|
||||
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
|
||||
return <span>{regionNames.of(countryCode)}</span>;
|
||||
}, []);
|
||||
|
||||
const columns: MRT_ColumnDef<DelegationWithNodeDetails>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -240,39 +374,52 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "node",
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
Header: <ColumnHeading>Node ID</ColumnHeading>,
|
||||
accessorKey: "delegation.node_id",
|
||||
size: 90,
|
||||
|
||||
Cell: ({ row }) =>
|
||||
row.original.delegation?.node_id ? (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">
|
||||
{row.original.delegation.node_id || "-"}
|
||||
</Typography>
|
||||
<Typography variant="body5">
|
||||
{row.original.node?.identity_key || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body4">
|
||||
{row.original.delegation.node_id || "-"}
|
||||
</Typography>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "identity_key",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Identity Key</ColumnHeading>,
|
||||
accessorKey: "delegation.node.identity_key",
|
||||
Cell: ({ row }) =>
|
||||
row.original.node?.identity_key ? (
|
||||
<Typography variant="body4">
|
||||
<Stack spacing={1}>
|
||||
{row.original.node?.identity_key || "-"}
|
||||
</Stack>
|
||||
</Typography>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "location",
|
||||
header: "Location",
|
||||
accessorKey: "node.countryCode",
|
||||
size: 160,
|
||||
Header: <ColumnHeading>Location</ColumnHeading>,
|
||||
Cell: ({ row }) =>
|
||||
row.original.node?.countryCode && row.original.node?.countryName ? (
|
||||
<Tooltip title={row.original.node?.countryName}>
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={row.original.node.countryCode}
|
||||
countryName={row.original.node.countryCode}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={row.original.node.countryCode}
|
||||
countryName={countryName(row.original.node?.countryName) || ""}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
@@ -282,6 +429,17 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
header: "Staked amount",
|
||||
accessorKey: "delegation.amount.amount",
|
||||
Header: <ColumnHeading>Stake</ColumnHeading>,
|
||||
size: 80,
|
||||
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const stakeA = Number.parseFloat(
|
||||
rowA.original.delegation.amount.amount,
|
||||
);
|
||||
const stakeB = Number.parseFloat(
|
||||
rowB.original.delegation.amount.amount,
|
||||
);
|
||||
return stakeA - stakeB;
|
||||
},
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{formatBigNum(+row.original.delegation.amount.amount / 1_000_000)}{" "}
|
||||
@@ -293,7 +451,14 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
id: "stakeSaturation",
|
||||
header: "Stake saturation",
|
||||
accessorKey: "node.stakeSaturation",
|
||||
size: 200,
|
||||
Header: <ColumnHeading>Stake saturation</ColumnHeading>,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const saturationA = rowA.original.node?.stakeSaturation || 0;
|
||||
const saturationB = rowB.original.node?.stakeSaturation || 0;
|
||||
console.log("sorting :>> ", saturationA, saturationB);
|
||||
return saturationA - saturationB;
|
||||
},
|
||||
Cell: ({ row }) =>
|
||||
row.original.node?.stakeSaturation ? (
|
||||
<Typography variant="body4">
|
||||
@@ -308,6 +473,8 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
header: "Favorite",
|
||||
accessorKey: "Favorite",
|
||||
enableColumnFilter: false,
|
||||
size: 80,
|
||||
|
||||
Header: (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<ColumnHeading>Favorite</ColumnHeading>
|
||||
@@ -335,24 +502,40 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
{
|
||||
id: "action",
|
||||
header: "Action",
|
||||
enableColumnFilter: false,
|
||||
Header: <ColumnHeading>Action</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<StakeActions
|
||||
nodeId={row.original.delegation?.node_id}
|
||||
nodeIdentityKey={row.original.node?.identity_key}
|
||||
onActionSelect={(action) => {
|
||||
handleActionSelect(
|
||||
action,
|
||||
row.original.delegation?.node_id,
|
||||
row.original.node?.identity_key || undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
size: 80,
|
||||
|
||||
enableColumnFilter: false,
|
||||
Cell: ({ row }) => {
|
||||
return (
|
||||
<Box>
|
||||
{row.original.pendingEvent ? (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={getTooltipTitle(row.original.pendingEvent)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Chip size="small" label="Pending events" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<StakeActions
|
||||
nodeId={row.original.delegation?.node_id}
|
||||
nodeIdentityKey={row.original.node?.identity_key}
|
||||
onActionSelect={(action) => {
|
||||
handleActionSelect(
|
||||
action,
|
||||
row.original.delegation?.node_id,
|
||||
row.original.node?.identity_key || undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[handleActionSelect, favorites],
|
||||
[handleActionSelect, favorites, getTooltipTitle, countryName],
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
@@ -365,12 +548,21 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
enableHiding: false,
|
||||
paginationDisplayMode: "pages",
|
||||
renderEmptyRowsFallback: () => (
|
||||
<Stack gap={3} sx={{ p: 5 }} justifyContent="center" alignItems="center">
|
||||
<Typography variant="body3">
|
||||
<Stack
|
||||
gap={3}
|
||||
sx={{ p: 5 }}
|
||||
justifyContent={isMobile ? "flex-start" : "center"}
|
||||
alignItems={isMobile ? "flex-start" : "center"}
|
||||
>
|
||||
<Typography variant="body3" width={isMobile ? 300 : "unset"}>
|
||||
You haven't staked on any nodes yet. Stake on a node to start
|
||||
earning rewnotards.
|
||||
earning rewards.
|
||||
</Typography>
|
||||
<Button variant="contained" size="large">
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Link href="/explorer" underline="none" color="inherit">
|
||||
Stake
|
||||
</Link>
|
||||
@@ -390,7 +582,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
|
||||
initialState: {
|
||||
columnPinning: { right: ["Action", "Favorite"] },
|
||||
columnPinning: isMobile ? {} : { right: ["Action", "Favorite"] }, // No pinning on mobile
|
||||
},
|
||||
|
||||
muiColumnActionsButtonProps: {
|
||||
@@ -402,6 +594,11 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
muiTablePaperProps: {
|
||||
elevation: 0,
|
||||
},
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
alignItems: "center",
|
||||
},
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor: "background.paper",
|
||||
@@ -411,6 +608,8 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
muiTableBodyCellProps: {
|
||||
sx: {
|
||||
border: "none",
|
||||
whiteSpace: "unset", // Allow text wrapping in body cells
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
@@ -443,7 +642,7 @@ const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading && <Loading />}
|
||||
{isDataLoading && <Loading />}
|
||||
<StakeModal
|
||||
nodeId={selectedNodeForStaking?.nodeId}
|
||||
identityKey={selectedNodeForStaking?.identityKey}
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import getNymNodes from "@/actions/getNymNodes";
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { CURRENT_EPOCH_REWARDS } from "@/app/api/urls";
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import StakeTable from "./StakeTable";
|
||||
|
||||
// Fetch function for epoch rewards
|
||||
const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch epoch rewards");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Utility function to calculate node saturation point
|
||||
function getNodeSaturationPoint(
|
||||
totalStake: number,
|
||||
@@ -37,7 +19,7 @@ function getNodeSaturationPoint(
|
||||
}
|
||||
|
||||
const ratio = (totalStake / saturation) * 100;
|
||||
return Number(ratio.toFixed());
|
||||
return Number.parseFloat(ratio.toFixed());
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
@@ -51,8 +33,12 @@ const mappedNymNodes = (
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
return {
|
||||
name: node.self_description.moniker,
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
@@ -60,7 +46,7 @@ const mappedNymNodes = (
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint || 0,
|
||||
stakeSaturation: +nodeSaturationPoint || 0,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -76,8 +62,6 @@ const StakeTableWithAction = () => {
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
});
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
@@ -87,23 +71,41 @@ const StakeTableWithAction = () => {
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: getNymNodes,
|
||||
staleTime: 60000,
|
||||
refetchInterval: 60000,
|
||||
queryFn: fetchObservatoryNodes,
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return <div>Loading stake table...</div>;
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
<Skeleton variant="text" height={100} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
return <div>Error loading stake table data. Please try again later.</div>;
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Error loading stake table data. Please try again later.
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const data = mappedNymNodes(nymNodes, epochRewardsData);
|
||||
|
||||
if (!epochRewardsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
|
||||
return <StakeTable nodes={data} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import NextEpochTime from "@/components/epochtime/EpochTime";
|
||||
import Grid2 from "@mui/material/Grid2";
|
||||
import NextEpochTime from "../../components/epochtime/EpochTime";
|
||||
import SubHeaderRowActions from "./SubHeaderRowActions";
|
||||
|
||||
const SubHeaderRow = () => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import type { NodeRewardDetails, ObservatoryBalance } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_BALANCES_URL } from "@/app/api/urls";
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "@/config";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import { fetchTotalStakerRewards } from "../../app/api";
|
||||
import type { NodeRewardDetails } from "../../app/api/types";
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import Loading from "../loading";
|
||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||
import RedeemRewardsModal from "../redeemRewards/RedeemRewardsModal";
|
||||
@@ -25,24 +25,6 @@ const fetchDelegations = async (
|
||||
return data.delegations;
|
||||
};
|
||||
|
||||
// Fetch total staker rewards
|
||||
const fetchTotalRewards = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
return Number(balances.rewards.staking_rewards.amount);
|
||||
};
|
||||
|
||||
const SubHeaderRowActions = () => {
|
||||
const [openRedeemRewardsModal, setOpenRedeemRewardsModal] =
|
||||
useState<boolean>(false);
|
||||
@@ -54,6 +36,8 @@ const SubHeaderRowActions = () => {
|
||||
const { address, nymClient } = useNymClient();
|
||||
const { getSigningCosmWasmClient } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch delegations using React Query
|
||||
const {
|
||||
data: delegations = [],
|
||||
@@ -63,8 +47,6 @@ const SubHeaderRowActions = () => {
|
||||
queryKey: ["delegations", address],
|
||||
queryFn: () => fetchDelegations(address || "", nymClient),
|
||||
enabled: !!address && !!nymClient, // Only fetch if address and nymClient are available
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
// Fetch total rewards using React Query
|
||||
@@ -72,14 +54,18 @@ const SubHeaderRowActions = () => {
|
||||
data: totalStakerRewards = 0,
|
||||
isLoading: isRewardsLoading,
|
||||
isError: isRewardsError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["totalRewards", address],
|
||||
queryFn: () => fetchTotalRewards(address || ""),
|
||||
queryKey: ["totalStakerRewards", address],
|
||||
queryFn: () => fetchTotalStakerRewards(address || ""),
|
||||
enabled: !!address, // Only fetch if address is available
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const handleRefetch = useCallback(async () => {
|
||||
refetch();
|
||||
queryClient.invalidateQueries(); // This will refetch ALL active queries
|
||||
}, [queryClient, refetch]);
|
||||
|
||||
const handleRedeemRewards = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setOpenRedeemRewardsModal(false);
|
||||
@@ -107,13 +93,18 @@ const SubHeaderRowActions = () => {
|
||||
fee,
|
||||
"Redeeming all rewards",
|
||||
);
|
||||
console.log("Rewards redeemed successfully:", result);
|
||||
|
||||
// Success state
|
||||
setIsLoading(false);
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Success",
|
||||
message: "All rewards have been redeemed successfully!",
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
tx: result?.transactionHash,
|
||||
|
||||
onClose: async () => {
|
||||
await handleRefetch();
|
||||
setInfoModalProps({ open: false });
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error redeeming rewards:", error);
|
||||
@@ -127,7 +118,13 @@ const SubHeaderRowActions = () => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [address, nymClient, delegations, getSigningCosmWasmClient]);
|
||||
}, [
|
||||
address,
|
||||
nymClient,
|
||||
delegations,
|
||||
getSigningCosmWasmClient,
|
||||
handleRefetch,
|
||||
]);
|
||||
|
||||
const handleRedeemRewardsButtonClick = () => {
|
||||
setOpenRedeemRewardsModal(true);
|
||||
@@ -153,7 +150,11 @@ const SubHeaderRowActions = () => {
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||
<Button variant="contained" onClick={handleRedeemRewardsButtonClick}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleRedeemRewardsButtonClick}
|
||||
disabled={totalStakerRewards === 0}
|
||||
>
|
||||
Redeem NYM
|
||||
</Button>
|
||||
{isLoading && <Loading />}
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { ObservatoryBalance } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_BALANCES_URL } from "@/app/api/urls";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchTotalStakerRewards } from "../../app/api";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
// Fetch function to get total staker rewards
|
||||
const fetchTotalStakerRewards = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the staking rewards amount
|
||||
return Number(balances.rewards.staking_rewards.amount);
|
||||
};
|
||||
|
||||
const TotalRewardsCard = () => {
|
||||
const { address } = useNymClient();
|
||||
|
||||
@@ -40,8 +19,6 @@ const TotalRewardsCard = () => {
|
||||
queryKey: ["totalStakerRewards", address],
|
||||
queryFn: () => fetchTotalStakerRewards(address || ""),
|
||||
enabled: !!address, // Only fetch if address exists
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -51,7 +28,7 @@ const TotalRewardsCard = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
<Skeleton variant="text" />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +36,7 @@ const TotalRewardsCard = () => {
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography variant="body2" color="error">
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load total rewards.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { ObservatoryBalance } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_BALANCES_URL } from "@/app/api/urls";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchBalances } from "../../app/api";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
// Fetch balances based on the address
|
||||
const fetchBalances = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Calculate total stake
|
||||
return (
|
||||
Number(balances.rewards.staking_rewards.amount) +
|
||||
Number(balances.delegated.amount)
|
||||
);
|
||||
};
|
||||
|
||||
const TotalStakeCard = () => {
|
||||
const { address } = useNymClient();
|
||||
|
||||
@@ -43,8 +19,6 @@ const TotalStakeCard = () => {
|
||||
queryKey: ["totalStake", address],
|
||||
queryFn: () => fetchBalances(address || ""),
|
||||
enabled: !!address, // Only fetch if address exists
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -54,7 +28,7 @@ const TotalStakeCard = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
<Skeleton variant="text" />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +36,7 @@ const TotalStakeCard = () => {
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography variant="body2" color="error">
|
||||
<Typography variant="h3" color="error">
|
||||
Failed to load total stake.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { validateAmount } from "@/utils/currency";
|
||||
import { z } from "zod";
|
||||
import { validateAmount } from "../../utils/currency";
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = "10";
|
||||
const fee = { gas: "1000000", amount: [{ amount: "1000000", denom: "unym" }] };
|
||||
@@ -36,7 +36,6 @@ const stakingSchema = z
|
||||
(data) => {
|
||||
const balance = Number.parseFloat(data.balance);
|
||||
const amount = Number.parseFloat(data.amount);
|
||||
console.log(balance);
|
||||
return balance - amount >= 0;
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import {
|
||||
Box,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
IconButton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import Cross from "../icons/Cross";
|
||||
import { WalletAddress } from "./WalletAddress";
|
||||
import { WalletBalance } from "./WalletBalance";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elips } from "@/components/icons/Elips";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Elips } from "../../components/icons/Elips";
|
||||
|
||||
export const trimAddress = (address = "", trimBy = 6) =>
|
||||
`${address.slice(0, trimBy)}...${address.slice(-trimBy)}`;
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import { Token } from "@/components/icons/Token";
|
||||
import useGetWalletBalance from "@/hooks/useGetWalletBalance";
|
||||
"use client";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Token } from "../../components/icons/Token";
|
||||
import useGetWalletBalance from "../../hooks/useGetWalletBalance";
|
||||
|
||||
export const WalletBalance = () => {
|
||||
const { formattedBalance } = useGetWalletBalance();
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
const { formattedBalance, isLoading, isError, refetch } =
|
||||
useGetWalletBalance();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Token />
|
||||
<Typography variant="h5" fontWeight={400}>
|
||||
Loading...
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (isError) {
|
||||
if (isWalletConnected) {
|
||||
refetch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Welcome to the allNym Network Explorer",
|
||||
"title": "Welcome to the Nym Network Explorer",
|
||||
"label": "Onboarding",
|
||||
"description": "Let's unpack the key features of the long awaited features together!",
|
||||
"attributes": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"text": "**Designed in a mobile first way, with integrated onboarding articles and live network data. The application is intentionally kept lightweight and barebones, while opening up a new social interaction layer of the mixnet. The NGM Explorer is currently in open beta release, which means that each released version of the app is live-tested by the core community of Nym node operators, where future features and updates are informed by feedback on the Node Operators forum."
|
||||
"text": "Designed in a mobile first way, with integrated onboarding articles and live network data. The application is intentionally kept lightweight and barebones, while opening up a new social interaction layer of the mixnet. The NGM Explorer is currently in open beta release, which means that each released version of the app is live-tested by the core community of Nym node operators, where future features and updates are informed by feedback on the Node Operators forum."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { PendingEpochEventKind } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const getEventsByAddress = (
|
||||
kind: PendingEpochEventKind,
|
||||
address: string,
|
||||
) => {
|
||||
if ("delegate" in kind && kind.delegate.owner === address) {
|
||||
return {
|
||||
kind: "delegate" as const,
|
||||
mixId: kind.delegate.node_id,
|
||||
amount: kind.delegate.amount,
|
||||
};
|
||||
}
|
||||
|
||||
if ("undelegate" in kind && kind.undelegate.owner === address) {
|
||||
return {
|
||||
kind: "undelegate" as const,
|
||||
mixId: kind.undelegate.node_id,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
export type PendingEvent = ReturnType<typeof getEventsByAddress>;
|
||||
|
||||
// Custom Hook for fetching pending events
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const usePendingEvents = (nymQueryClient: any, address: string | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: ["pendingEvents", address], // Query key to uniquely identify this query
|
||||
queryFn: async () => {
|
||||
if (!nymQueryClient || !address) {
|
||||
throw new Error("Missing required dependencies");
|
||||
}
|
||||
|
||||
const response = await nymQueryClient.getPendingEpochEvents({});
|
||||
const pendingEvents: PendingEvent[] = [];
|
||||
|
||||
for (const e of response.events) {
|
||||
const event = getEventsByAddress(e.event.kind, address);
|
||||
if (event) {
|
||||
pendingEvents.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return pendingEvents;
|
||||
},
|
||||
enabled: !!nymQueryClient && !!address, // Prevents execution if dependencies are missing
|
||||
});
|
||||
};
|
||||
|
||||
export default usePendingEvents;
|
||||
@@ -1,46 +1,42 @@
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { unymToNym } from "@/utils/currency";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../config";
|
||||
import { unymToNym } from "../utils/currency";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fetchNYMBalance = async (address: string, getCosmWasmClient: any) => {
|
||||
if (!address) return { NYMBalance: "0", formattedBalance: "-" };
|
||||
|
||||
const account = await getCosmWasmClient();
|
||||
const uNYMBalance = await account.getBalance(address, "unym");
|
||||
const NYMBalance = unymToNym(uNYMBalance.amount);
|
||||
if (!NYMBalance) return;
|
||||
const formattedBalance = Intl.NumberFormat().format(+NYMBalance);
|
||||
|
||||
return { NYMBalance, formattedBalance };
|
||||
};
|
||||
|
||||
const useGetWalletBalance = () => {
|
||||
const [balance, setBalance] = useState<string>("0");
|
||||
const [formattedBalance, setFormattedBalance] = useState<string>("-");
|
||||
const { getCosmWasmClient, address } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const getNYMBalance = useCallback(
|
||||
async (address: string) => {
|
||||
const account = await getCosmWasmClient();
|
||||
const uNYMBalance = await account.getBalance(address, "unym");
|
||||
const NYMBalance = unymToNym(uNYMBalance.amount);
|
||||
if (!NYMBalance) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedBalance = Intl.NumberFormat().format(+NYMBalance);
|
||||
return {
|
||||
NYMBalance,
|
||||
formattedBalance,
|
||||
};
|
||||
},
|
||||
[getCosmWasmClient],
|
||||
);
|
||||
const {
|
||||
data = { NYMBalance: "0", formattedBalance: "-" },
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["nymBalance", address],
|
||||
queryFn: () => fetchNYMBalance(address || "", getCosmWasmClient),
|
||||
enabled: !!address, // Only fetch if address exists
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
getNYMBalance(address)
|
||||
.then((balance) => {
|
||||
setFormattedBalance(balance?.formattedBalance || "-");
|
||||
setBalance(balance?.NYMBalance || "0");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to get balance", e);
|
||||
});
|
||||
}, [address, getNYMBalance]);
|
||||
|
||||
return { balance, formattedBalance };
|
||||
return {
|
||||
balance: data.NYMBalance,
|
||||
formattedBalance: data.formattedBalance,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch, // Expose refetch function if needed
|
||||
};
|
||||
};
|
||||
|
||||
export default useGetWalletBalance;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "@/config";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { contracts } from "@nymproject/contract-clients";
|
||||
import type {
|
||||
@@ -8,6 +7,7 @@ import type {
|
||||
MixnetQueryClient,
|
||||
} from "@nymproject/contract-clients/Mixnet.client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../config";
|
||||
|
||||
export const useNymClient = () => {
|
||||
const [nymClient, setNymClient] = useState<MixnetClient>();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { fetchCurrentEpoch } from "@/app/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { differenceInMilliseconds } from "date-fns";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type EpochStatus = "active" | "pending";
|
||||
|
||||
export type EpochResponseData =
|
||||
| Awaited<ReturnType<typeof fetchCurrentEpoch>>
|
||||
| undefined;
|
||||
|
||||
type EpochContext = {
|
||||
epochStatus: EpochStatus;
|
||||
data: EpochResponseData;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
epochStatus: "pending" as const,
|
||||
data: undefined,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const EpochContext = createContext<EpochContext>(initialState);
|
||||
|
||||
const checkIsEpochTimeValid = (epochEndTime: string) =>
|
||||
new Date(epochEndTime) >= new Date();
|
||||
|
||||
const calculateRefetchInterval = (epochEndTime: string) => {
|
||||
return differenceInMilliseconds(new Date(epochEndTime), new Date());
|
||||
};
|
||||
|
||||
const useEpochContext = () => {
|
||||
const context = useContext(EpochContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useEpochContext must be used within a EpochProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
const EpochProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [epochStatus, setEpochStatus] = useState<EpochStatus>("pending");
|
||||
|
||||
const QueryClient = useQueryClient();
|
||||
|
||||
const { data, isError, isLoading } = useQuery({
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ["currentEpoch"],
|
||||
queryFn: fetchCurrentEpoch,
|
||||
refetchInterval: ({ state }) => {
|
||||
// refetchInterval can be set dynamically based on the current state
|
||||
|
||||
// if there is no data, refetch in 30 secs
|
||||
if (!state.data) {
|
||||
return 30_000;
|
||||
}
|
||||
|
||||
const isEpochTimeValid = checkIsEpochTimeValid(
|
||||
state.data.current_epoch_end.toString(),
|
||||
);
|
||||
|
||||
// if epoch time is not valid (i.e current_time > epoch_start_time) refetch in 30 secs
|
||||
if (!isEpochTimeValid) {
|
||||
setEpochStatus("pending");
|
||||
return 30_000;
|
||||
}
|
||||
|
||||
// if epoch time is valid, refetch based on the epoch end time
|
||||
const newRefetchInterval = calculateRefetchInterval(
|
||||
state.data.current_epoch_end.toString(),
|
||||
);
|
||||
|
||||
setEpochStatus("active");
|
||||
return newRefetchInterval;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const refreshQueries = async () => {
|
||||
await QueryClient.invalidateQueries({
|
||||
predicate: (query) => query.queryKey[0] !== "currentEpoch",
|
||||
});
|
||||
};
|
||||
|
||||
// when new epoch starts, refresh all data
|
||||
if (epochStatus === "active") {
|
||||
refreshQueries();
|
||||
}
|
||||
}, [epochStatus, QueryClient]);
|
||||
|
||||
const value = {
|
||||
epochStatus,
|
||||
data,
|
||||
isError,
|
||||
isLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
<EpochContext.Provider value={value}>{children}</EpochContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { EpochProvider, useEpochContext };
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { lightTheme } from "@/theme/theme";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { ThemeProvider as MUIThemeProvider } from "@mui/material";
|
||||
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
|
||||
import { lightTheme } from "../theme/theme";
|
||||
|
||||
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import CosmosKitProvider from "./CosmosKitProvider";
|
||||
import { EpochProvider } from "./EpochProvider";
|
||||
import { QueryProvider } from "./QueryProvider";
|
||||
import ThemeProvider from "./ThemeProvider";
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<CosmosKitProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</CosmosKitProvider>
|
||||
<QueryProvider>
|
||||
<EpochProvider>
|
||||
<CosmosKitProvider>{children}</CosmosKitProvider>
|
||||
</EpochProvider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { labGrotesque, labGrotesqueMono } from "@/fonts";
|
||||
import { labGrotesque, labGrotesqueMono } from "../fonts";
|
||||
|
||||
export const headings = {
|
||||
display: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
declare module "@tanstack/react-query";
|
||||
Reference in New Issue
Block a user