diff --git a/explorer-nextjs/app/api/explorer/index.ts b/explorer-nextjs/app/api/explorer/index.ts
new file mode 100644
index 0000000000..6f30ae06d7
--- /dev/null
+++ b/explorer-nextjs/app/api/explorer/index.ts
@@ -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();
+// }
diff --git a/explorer-nextjs/app/api/urls.ts b/explorer-nextjs/app/api/urls.ts
new file mode 100644
index 0000000000..39952a07ef
--- /dev/null
+++ b/explorer-nextjs/app/api/urls.ts
@@ -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";
diff --git a/explorer-nextjs/app/components/ExplorerCard.tsx b/explorer-nextjs/app/components/ExplorerCard.tsx
index bd7d16faae..9ab2ea7e89 100644
--- a/explorer-nextjs/app/components/ExplorerCard.tsx
+++ b/explorer-nextjs/app/components/ExplorerCard.tsx
@@ -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 (
- {percentage}% (24H)
+
+ {percentage}% (24H)
+
);
};
@@ -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 = ({
{overTitle && (
-
+
{overTitle}
)}
diff --git a/explorer-nextjs/app/components/ExplorerProgressBar.tsx b/explorer-nextjs/app/components/ExplorerProgressBar.tsx
index 2fe98ae613..af9cf54db8 100644
--- a/explorer-nextjs/app/components/ExplorerProgressBar.tsx
+++ b/explorer-nextjs/app/components/ExplorerProgressBar.tsx
@@ -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 (
- {title && {title}}
+ {title && (
+
+ {title}
+
+ )}
-
+
{showEpoch && (
- Start: {new Date(start).toLocaleString()}
- End: {new Date(end).toLocaleString()}
+
+ START:
+ {formatDate(startTime)}
+
+
+ END:
+ {formatDate(endTime)}
+
)}
diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx
index 8ebada4a88..7dfc19d7f6 100644
--- a/explorer-nextjs/app/page.tsx
+++ b/explorer-nextjs/app/page.tsx
@@ -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(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 = () => {
+
+
+
+
+
+
router.push("/network-components/mixnodes")}
@@ -218,6 +292,4 @@ const PageOverview = () => {
);
-};
-
-export default PageOverview;
+}