WIP
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
EXPLORER_API,
|
||||
COSMOS_API,
|
||||
VALIDATOR_API_EPOCH,
|
||||
VALIDATOR_API_SUPPLY,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_BASE,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
} from "../urls";
|
||||
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: any;
|
||||
nymNodesData: any;
|
||||
packetsAndStakingData: any;
|
||||
currentEpochData: any;
|
||||
currentEpochRewardsData: any;
|
||||
}
|
||||
|
||||
export interface ExplorerCache {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Extend the global object with our custom property
|
||||
var explorerCache: ExplorerCache | undefined;
|
||||
}
|
||||
|
||||
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH
|
||||
const fetchCurrentEpoch = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH REWARDS
|
||||
const fetchCurrentEpochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CIRCULATING NYM SUPPLY
|
||||
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH PACKETS AND STAKING
|
||||
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
currentEpochRes,
|
||||
currentEpochRewardsRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchCurrentEpoch,
|
||||
fetchCurrentEpochRewards,
|
||||
]);
|
||||
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
currentEpochRes.json(),
|
||||
currentEpochRewardsRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
// makes sure the cache exists in global memory
|
||||
let doUpdate = false;
|
||||
const now = new Date();
|
||||
if (!global.explorerCache) {
|
||||
global.explorerCache = {};
|
||||
doUpdate = true;
|
||||
}
|
||||
if (
|
||||
global.explorerCache.lastUpdated &&
|
||||
now.getDate() - global.explorerCache.lastUpdated.getDate() >
|
||||
CACHE_TIME_SECONDS
|
||||
) {
|
||||
doUpdate = true;
|
||||
}
|
||||
|
||||
// if the cache has expired or never existed, get it from API's
|
||||
if (doUpdate) {
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await getExplorerData();
|
||||
|
||||
packetsAndStakingData.pop();
|
||||
|
||||
global.explorerCache.data = {
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
};
|
||||
global.explorerCache.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheExplorerData() {
|
||||
await ensureCacheExists();
|
||||
|
||||
if (!global.explorerCache?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return global.explorerCache.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom API route that returns metadata from Strapi about images: height, width, strapi download url.
|
||||
*
|
||||
* The response from Strapi is cached in memory for CACHE_TIME_SECONDS.
|
||||
*/
|
||||
// export default async function handler(
|
||||
// req: NextApiRequest,
|
||||
// res: NextApiResponse
|
||||
// ) {
|
||||
// // return cached data
|
||||
// const data = await getCacheExplorerData();
|
||||
// if (data) {
|
||||
// res.status(200).json(data);
|
||||
// res.end();
|
||||
// }
|
||||
|
||||
// // catch-all
|
||||
// res.status(404).end();
|
||||
// }
|
||||
@@ -0,0 +1,18 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const COSMOS_API =
|
||||
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
|
||||
export const VALIDATOR_API_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
|
||||
export const CURRENT_EPOCH =
|
||||
" https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
@@ -8,15 +8,17 @@ import {
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
priceWentUp: boolean;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps
|
||||
): ReactElement => {
|
||||
const { percentage, priceWentUp } = props;
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box mb={3}>
|
||||
<Typography sx={{ color: "#00CA33" }}>{percentage}% (24H)</Typography>
|
||||
<Typography sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}>
|
||||
{percentage}% (24H)
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -66,7 +68,7 @@ const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
|
||||
|
||||
type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
title?: string;
|
||||
title?: string | number;
|
||||
upDownLine?: ICardUpDownPriceLineProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
@@ -90,7 +92,7 @@ export const ExplorerCard: FC<ContentCardProps> = ({
|
||||
<Card onClick={onClick} sx={{ height: "100%" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3}>
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -6,22 +6,21 @@ import { Typography } from "@mui/material";
|
||||
export interface IExplorerProgressBarProps {
|
||||
title?: string;
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
end: string; // End timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
export const ExplorerProgressBar = (props: IExplorerProgressBarProps) => {
|
||||
const { start, end, showEpoch, title } = props;
|
||||
const { start, showEpoch, title } = props;
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Parse the timestamps
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = new Date(end).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate timestamps
|
||||
if (isNaN(startTime) || isNaN(endTime)) {
|
||||
console.error("Invalid start or end timestamp:", { start, end });
|
||||
// Validate start timestamp
|
||||
if (isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,17 +48,50 @@ export const ExplorerProgressBar = (props: IExplorerProgressBarProps) => {
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start, end]);
|
||||
}, [start]);
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based
|
||||
const year = date.getFullYear();
|
||||
return `${hours}:${minutes}, ${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{title && <Typography mb={2}>{title}</Typography>}
|
||||
{title && (
|
||||
<Typography mb={2} textTransform={"uppercase"}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<LinearProgress variant="determinate" value={progress} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#14E76F",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showEpoch && (
|
||||
<Box mt={2}>
|
||||
<Typography>Start: {new Date(start).toLocaleString()}</Typography>
|
||||
<Typography>End: {new Date(end).toLocaleString()}</Typography>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>START:</Typography>
|
||||
<Typography> {formatDate(startTime)}</Typography>
|
||||
</Box>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>END:</Typography>
|
||||
<Typography> {formatDate(endTime)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Grid, Link, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
@@ -18,6 +18,8 @@ import { formatNumber } from "@/app/utils";
|
||||
import { useMainContext } from "./context/main";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ExplorerCard } from "./components/ExplorerCard";
|
||||
import type { GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import { ExplorerData, getCacheExplorerData } from "./api/explorer";
|
||||
|
||||
// type ContentCardProps = {
|
||||
// overTitle?: string;
|
||||
@@ -36,13 +38,13 @@ const explorerCard = {
|
||||
title: "SINGLE",
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
priceWentUp: true,
|
||||
numberWentUp: true,
|
||||
},
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
priceWentUp: true,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
@@ -73,19 +75,85 @@ const explorerCard = {
|
||||
purpleLineNumericData: 4,
|
||||
},
|
||||
],
|
||||
progressBar: {
|
||||
title: "Current NGM epoch",
|
||||
start: "2024-11-24T13:26:19Z",
|
||||
end: "2024-11-24T14:26:19Z",
|
||||
showEpoch: true,
|
||||
},
|
||||
|
||||
paragraph: "Additional line",
|
||||
};
|
||||
export const DATA_REVALIDATE = 60;
|
||||
|
||||
const PageOverview = () => {
|
||||
export default function PageOverview() {
|
||||
const [explorerData, setExplorerData] = useState<ExplorerData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const data = await getCacheExplorerData();
|
||||
setExplorerData(data);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
console.log("explorerData :>> ", explorerData);
|
||||
const currentEpochStart =
|
||||
explorerData?.currentEpochData.current_epoch_start || "";
|
||||
|
||||
const progressBar = {
|
||||
title: "Current NGM epoch",
|
||||
start: currentEpochStart,
|
||||
showEpoch: true,
|
||||
};
|
||||
|
||||
const formatBigNum = (num: number) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B";
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
||||
}
|
||||
return num;
|
||||
}
|
||||
};
|
||||
|
||||
const packetsSentLast24H =
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_sent;
|
||||
|
||||
const packetsSentPrevious24H =
|
||||
explorerData?.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_sent;
|
||||
|
||||
const calculatePercentageChange = (last24H: number, previous24H: number) => {
|
||||
if (previous24H === 0) {
|
||||
throw new Error(
|
||||
"Cannot calculate percentage change when yesterday's value is zero."
|
||||
);
|
||||
}
|
||||
|
||||
const change = ((last24H - previous24H) / previous24H) * 100;
|
||||
|
||||
return parseFloat(change.toFixed(2));
|
||||
};
|
||||
|
||||
const percentage = calculatePercentageChange(
|
||||
packetsSentLast24H,
|
||||
packetsSentPrevious24H
|
||||
);
|
||||
|
||||
const noiseCard = {
|
||||
overTitle: "Noise generated last 24h",
|
||||
title: formatBigNum(packetsSentLast24H) || "",
|
||||
upDownLine: {
|
||||
percentage: Math.abs(percentage),
|
||||
numberWentUp: percentage > 0,
|
||||
},
|
||||
};
|
||||
|
||||
const {
|
||||
summaryOverview,
|
||||
gateways,
|
||||
@@ -107,6 +175,12 @@ const PageOverview = () => {
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...explorerCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard progressBar={progressBar} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...noiseCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => router.push("/network-components/mixnodes")}
|
||||
@@ -218,6 +292,4 @@ const PageOverview = () => {
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageOverview;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user