Add tanstack react-query for refetching data on epoch change
This commit is contained in:
@@ -8,9 +8,10 @@ import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import ExplorerCard from "@/components/cards/ExplorerCard";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import DelegationsTable from "@/components/nodeTable/DelegationsTable";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
import { NodeChatCard } from "@/components/nymNodePageComponents/ChatCard";
|
||||
import DelegationsTable from "@/components/nymNodePageComponents/DelegationsTable";
|
||||
import NodeDelegationsCard from "@/components/nymNodePageComponents/NodeDelegationsCard";
|
||||
import { NodeMetricsCard } from "@/components/nymNodePageComponents/NodeMetricsCard";
|
||||
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
|
||||
import { NodeRewardsCard } from "@/components/nymNodePageComponents/NodeRewardsCard";
|
||||
@@ -25,15 +26,6 @@ export default async function NymNode({
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
try {
|
||||
const epochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
const epochRewardsData: ExplorerData["currentEpochRewardsData"] =
|
||||
await epochRewards.json();
|
||||
|
||||
const id = Number((await params).id);
|
||||
|
||||
const observatoryResponse = await fetch(DATA_OBSERVATORY_NODES_URL, {
|
||||
@@ -60,20 +52,6 @@ export default async function NymNode({
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeDelegationsResponse = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
},
|
||||
);
|
||||
|
||||
const delegations = await nodeDelegationsResponse.json();
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
@@ -98,77 +76,53 @@ export default async function NymNode({
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
{observatoryNymNode && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard nodeInfo={observatoryNymNode} />
|
||||
</Grid>
|
||||
)}
|
||||
{observatoryNymNode.rewarding_details && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard
|
||||
rewardDetails={observatoryNymNode.rewarding_details}
|
||||
nodeInfo={observatoryNymNode}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{observatoryNymNode && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<QualityIndicatorsCard nodeInfo={observatoryNymNode} />
|
||||
</Grid>
|
||||
)}
|
||||
{observatoryNymNode.rewarding_details && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeRewardsCard
|
||||
rewardDetails={observatoryNymNode.rewarding_details}
|
||||
nodeInfo={observatoryNymNode}
|
||||
epochRewardsData={epochRewardsData}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{observatoryNymNode && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeMetricsCard
|
||||
nodeInfo={observatoryNymNode}
|
||||
epochRewardsData={epochRewardsData}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{delegations && (
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
}}
|
||||
>
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<DelegationsTable delegations={delegations} />
|
||||
</ExplorerCard>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<QualityIndicatorsCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeRewardsCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeMetricsCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
}}
|
||||
>
|
||||
<NodeDelegationsCard id={id} />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
size={{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
NYM_NODES_DESCRIBED,
|
||||
NYM_NODE_DESCRIPTION,
|
||||
@@ -204,3 +206,51 @@ export async function getCacheExplorerData() {
|
||||
|
||||
return (global as ExplorerCache)?.explorerCache?.data || null;
|
||||
}
|
||||
|
||||
export const fetchEpochRewards = async () => {
|
||||
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();
|
||||
};
|
||||
|
||||
export const fetchObservatoryNodes = async () => {
|
||||
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");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchNodeDelegations = async (id: number) => {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch delegations");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
@@ -8,9 +8,8 @@ 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 { QueryProvider } from "@/components/queryProvider/QueryProvider";
|
||||
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { Suspense } from "react";
|
||||
|
||||
@@ -45,9 +44,7 @@ export default async function Home() {
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Suspense fallback={<CardSkeleton sx={{ width: "100%" }} />}>
|
||||
<QueryProvider>
|
||||
<CurrentEpochCard />
|
||||
</QueryProvider>
|
||||
<CurrentEpochCard />
|
||||
</Suspense>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { CURRENT_EPOCH } from "@/app/api/urls";
|
||||
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";
|
||||
|
||||
export const fetcNextEpoch = async () => {
|
||||
// Fetch function for the next epoch
|
||||
const fetchNextEpoch = async () => {
|
||||
const res = await fetch(CURRENT_EPOCH, {
|
||||
next: { revalidate: 60 },
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
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,
|
||||
@@ -18,23 +29,47 @@ export const fetcNextEpoch = async () => {
|
||||
return { data, dateTime };
|
||||
};
|
||||
|
||||
const NextEpochTime = async () => {
|
||||
try {
|
||||
const epoch = await fetcNextEpoch();
|
||||
const formattedDate = format(epoch.dateTime, "HH:mm:ss");
|
||||
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
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Next epoch: {formattedDate}
|
||||
Loading next epoch...
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Failed to load next epoch.
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const formattedDate = format(data.dateTime, "HH:mm:ss");
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Next epoch: {formattedDate}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextEpochTime;
|
||||
|
||||
@@ -16,9 +16,7 @@ const fetchCurrentEpoch = async (): Promise<CurrentEpochData> => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch current epoch data: ${response.statusText}`,
|
||||
);
|
||||
throw new Error("Failed to fetch current epoch data");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
"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 { useQuery } from "@tanstack/react-query";
|
||||
import NodeTable from "./NodeTable";
|
||||
|
||||
async function fetchEpochRewards() {
|
||||
try {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
// 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: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching epoch rewards:", error);
|
||||
throw new Error("Failed to fetch epoch rewards data");
|
||||
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,
|
||||
stakeSaturationPoint: string,
|
||||
@@ -39,6 +41,7 @@ function getNodeSaturationPoint(
|
||||
return Number(ratio.toFixed());
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
@@ -66,23 +69,45 @@ const mappedNymNodes = (
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const NodeTableWithAction = async () => {
|
||||
try {
|
||||
// Fetch the epoch rewards data
|
||||
const epochRewardsData: ExplorerData["currentEpochRewardsData"] =
|
||||
await fetchEpochRewards();
|
||||
const NodeTableWithAction = () => {
|
||||
// Use React Query to fetch epoch rewards
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
});
|
||||
|
||||
// Fetch the Nym nodes
|
||||
const nodes = await getNymNodes();
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: getNymNodes,
|
||||
staleTime: 60000,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
|
||||
// Map the nodes with the rewards data
|
||||
const data = mappedNymNodes(nodes, epochRewardsData);
|
||||
|
||||
return <NodeTable nodes={data} />;
|
||||
} catch (error) {
|
||||
console.error("Error in NodeTableWithAction:", error);
|
||||
return <div>Error loading data.</div>; // Render error fallback UI
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
return <div>Error loading data. Please try again later.</div>;
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const data = mappedNymNodes(nymNodes, epochRewardsData);
|
||||
|
||||
return <NodeTable nodes={data} />;
|
||||
};
|
||||
|
||||
export default NodeTableWithAction;
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
import type { IObservatoryNode, RewardingDetails } from "@/app/api/types";
|
||||
"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 { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface IBasicInfoCardProps {
|
||||
rewardDetails: RewardingDetails;
|
||||
nodeInfo: IObservatoryNode;
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
export const BasicInfoCard = (props: IBasicInfoCardProps) => {
|
||||
const { rewardDetails, nodeInfo } = props;
|
||||
// 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 },
|
||||
});
|
||||
|
||||
const timeBonded = nodeInfo
|
||||
? format(
|
||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
)
|
||||
: "-";
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch observatory nodes");
|
||||
}
|
||||
|
||||
const selfBond = formatBigNum(Number(rewardDetails.operator) / 1_000_000);
|
||||
const selfBondFormated = `${selfBond} NYM`;
|
||||
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
|
||||
});
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography>Loading...</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (isError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography>Failed to load node information.</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Derived data from nodeInfo
|
||||
const timeBonded = format(
|
||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
|
||||
const selfBond = formatBigNum(
|
||||
Number(nodeInfo.rewarding_details.operator) / 1_000_000,
|
||||
);
|
||||
const selfBondFormatted = `${selfBond} NYM`;
|
||||
|
||||
const totalStake = formatBigNum(Number(nodeInfo.total_stake) / 1_000_000);
|
||||
const totalStakeFormated = `${totalStake} NYM`;
|
||||
const totalStakeFormatted = `${totalStake} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Stack gap={1}>
|
||||
@@ -68,15 +120,15 @@ export const BasicInfoCard = (props: IBasicInfoCardProps) => {
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakers"
|
||||
value={rewardDetails.unique_delegations.toString()}
|
||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Self bonded"
|
||||
value={selfBondFormated}
|
||||
value={selfBondFormatted}
|
||||
/>
|
||||
<ExplorerListItem row label="Total stake" value={totalStakeFormated} />
|
||||
<ExplorerListItem row label="Total stake" value={totalStakeFormatted} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import type { NodeRewardDetails } from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import DelegationsTable from "./DelegationsTable";
|
||||
|
||||
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,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nodeDelegations", id],
|
||||
queryFn: () => fetchNodeDelegations(id),
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
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} />}
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeDelegationsCard;
|
||||
@@ -1,20 +1,99 @@
|
||||
"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 ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface INodeMetricsCardProps {
|
||||
nodeInfo: IObservatoryNode;
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"];
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
export const NodeMetricsCard = async (props: INodeMetricsCardProps) => {
|
||||
const { nodeInfo, epochRewardsData } = props;
|
||||
// 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",
|
||||
},
|
||||
});
|
||||
|
||||
function getActiveSetProbability(
|
||||
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,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading: isNodeLoading,
|
||||
isError: isNodeError,
|
||||
} = 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>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isNodeError || !nodeInfo || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<div>Failed to load data</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Function to calculate active set probability
|
||||
const getActiveSetProbability = (
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string,
|
||||
): string {
|
||||
): string => {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
@@ -30,15 +109,12 @@ export const NodeMetricsCard = async (props: INodeMetricsCardProps) => {
|
||||
return "Medium";
|
||||
}
|
||||
return "Low";
|
||||
}
|
||||
};
|
||||
|
||||
const activeSetProb =
|
||||
nodeInfo && epochRewardsData
|
||||
? getActiveSetProbability(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
)
|
||||
: "N/A";
|
||||
const activeSetProb = getActiveSetProbability(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
@@ -48,23 +124,19 @@ export const NodeMetricsCard = async (props: INodeMetricsCardProps) => {
|
||||
label="Node ID."
|
||||
value={nodeInfo.node_id.toString()}
|
||||
/>
|
||||
<>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeInfo.description.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeInfo.description.build_information.build_version}
|
||||
/>
|
||||
</>
|
||||
{epochRewardsData && (
|
||||
<ExplorerListItem row label="Active set Prob." value={activeSetProb} />
|
||||
)}
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeInfo.description.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeInfo.description.build_information.build_version}
|
||||
/>
|
||||
<ExplorerListItem row label="Active set Prob." value={activeSetProb} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
|
||||
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 { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RandomAvatar } from "react-random-avatars";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -16,11 +18,27 @@ import { fee } from "../staking/schemas";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
|
||||
interface INodeProfileCardProps {
|
||||
nodeInfo: IObservatoryNode;
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
const { nodeInfo } = props;
|
||||
// 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();
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
@@ -32,6 +50,18 @@ export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
identityKey: string;
|
||||
}>();
|
||||
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading: isNodeLoading,
|
||||
isError: isNodeError,
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const handleStakeOnNode = async ({
|
||||
nodeId,
|
||||
amount,
|
||||
@@ -57,7 +87,6 @@ export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
title: "Success",
|
||||
message: "This operation can take up to one hour to process",
|
||||
tx: tx?.transactionHash,
|
||||
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -95,12 +124,30 @@ export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedNodeForStaking({
|
||||
nodeId: nodeInfo.node_id,
|
||||
identityKey: nodeInfo.identity_key,
|
||||
});
|
||||
if (nodeInfo) {
|
||||
setSelectedNodeForStaking({
|
||||
nodeId: nodeInfo.node_id,
|
||||
identityKey: nodeInfo.identity_key,
|
||||
});
|
||||
}
|
||||
}, [isWalletConnected, nodeInfo]);
|
||||
|
||||
if (isNodeLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<div>Loading...</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNodeError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<div>Failed to load node information.</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Stack gap={1}>
|
||||
|
||||
@@ -1,33 +1,120 @@
|
||||
"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 { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface INodeRewardsCardProps {
|
||||
rewardDetails: RewardingDetails;
|
||||
nodeInfo?: IObservatoryNode;
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"];
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
export const NodeRewardsCard = (props: INodeRewardsCardProps) => {
|
||||
const { rewardDetails, epochRewardsData, nodeInfo } = props;
|
||||
// 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",
|
||||
},
|
||||
});
|
||||
|
||||
const operatorRewards = Number(rewardDetails.operator) / 1000000;
|
||||
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 {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
});
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading: isNodeLoading,
|
||||
isError: isNodeError,
|
||||
} = 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="Node rewards (last epoch/hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<div>Loading...</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isNodeError || !nodeInfo || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Node rewards (last epoch/hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<div>Failed to load data</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Extract reward details
|
||||
const rewardDetails: RewardingDetails = nodeInfo.rewarding_details;
|
||||
|
||||
// Calculated data
|
||||
const operatorRewards = Number(rewardDetails.operator) / 1_000_000;
|
||||
const operatorRewardsFormated = `${operatorRewards} NYM`;
|
||||
|
||||
const profitMarginPercent =
|
||||
Number(rewardDetails.cost_params.profit_margin_percent) * 100;
|
||||
|
||||
const profitMarginPercentFormated = `${profitMarginPercent}%`;
|
||||
|
||||
const operatingCosts =
|
||||
Number(rewardDetails.cost_params.interval_operating_cost.amount) / 1000000;
|
||||
Number(rewardDetails.cost_params.interval_operating_cost.amount) /
|
||||
1_000_000;
|
||||
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
||||
|
||||
function getNodeSaturationPoint(
|
||||
const getNodeSaturationPoint = (
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string,
|
||||
): string {
|
||||
): string => {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
@@ -37,36 +124,24 @@ export const NodeRewardsCard = (props: INodeRewardsCardProps) => {
|
||||
const ratio = (totalStake / saturation) * 100;
|
||||
|
||||
return `${ratio.toFixed()}%`;
|
||||
}
|
||||
};
|
||||
|
||||
const nodeSaturationPoint =
|
||||
nodeInfo && epochRewardsData
|
||||
? getNodeSaturationPoint(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
)
|
||||
: "N/A";
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Node rewards(last epoch/hour)" sx={{ height: "100%" }}>
|
||||
{/* <ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Total rew."
|
||||
value={totalRewardsFormated}
|
||||
/> */}
|
||||
<ExplorerCard
|
||||
label="Node rewards (last epoch/hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Operator rew."
|
||||
value={operatorRewardsFormated}
|
||||
/>
|
||||
{/* <ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Staker rew."
|
||||
value={stakerRewardsFormated}
|
||||
/> */}
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
|
||||
@@ -6,38 +6,69 @@ import type {
|
||||
LastProbeResult,
|
||||
NodeDescription,
|
||||
} from "@/app/api/types";
|
||||
import { DATA_OBSERVATORY_NODES_URL } from "@/app/api/urls";
|
||||
import { Chip, Stack } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import StarRating from "../starRating/StarRating";
|
||||
|
||||
interface IQualityIndicatorsCardProps {
|
||||
nodeInfo: IObservatoryNode;
|
||||
id: number; // Node ID
|
||||
}
|
||||
|
||||
type NodeDescriptionNotNull = NonNullable<NodeDescription>;
|
||||
type DelcaredRoleKey = keyof NodeDescriptionNotNull["declared_role"];
|
||||
type DeclaredRoleKey = keyof NodeDescriptionNotNull["declared_role"];
|
||||
type RoleString = "Entry Node" | "Exit IPR Node" | "Exit NR Node" | "Mix Node";
|
||||
|
||||
const roleMapping: Record<DelcaredRoleKey, RoleString> = {
|
||||
const roleMapping: Record<DeclaredRoleKey, RoleString> = {
|
||||
entry: "Entry Node",
|
||||
exit_ipr: "Exit IPR Node",
|
||||
exit_nr: "Exit NR Node",
|
||||
mixnode: "Mix Node",
|
||||
};
|
||||
|
||||
function getNodeRoles(
|
||||
// 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[] {
|
||||
const activeRoles = Object.entries(declaredRoles)
|
||||
): RoleString[] => {
|
||||
return Object.entries(declaredRoles)
|
||||
.filter(([, isActive]) => isActive)
|
||||
.map(([role]) => roleMapping[role as DelcaredRoleKey]);
|
||||
.map(([role]) => roleMapping[role as DeclaredRoleKey]);
|
||||
};
|
||||
|
||||
return activeRoles;
|
||||
}
|
||||
|
||||
function calculateQualityOfServiceStars(quality: number): number {
|
||||
const calculateQualityOfServiceStars = (quality: number): number => {
|
||||
switch (true) {
|
||||
case quality < 0.3:
|
||||
return 1;
|
||||
@@ -48,9 +79,9 @@ function calculateQualityOfServiceStars(quality: number): number {
|
||||
default:
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function calculateConfigScoreStars(probeResult: LastProbeResult): number {
|
||||
const calculateConfigScoreStars = (probeResult: LastProbeResult): number => {
|
||||
const { as_entry, as_exit } = probeResult.outcome;
|
||||
|
||||
if (as_entry && as_exit) {
|
||||
@@ -78,53 +109,12 @@ function calculateConfigScoreStars(probeResult: LastProbeResult): number {
|
||||
}
|
||||
}
|
||||
|
||||
if (as_entry) {
|
||||
const { can_connect, can_route } = as_entry;
|
||||
const entryScore = [can_connect, can_route].filter(Boolean).length;
|
||||
return 1; // Default case
|
||||
};
|
||||
|
||||
switch (entryScore) {
|
||||
case 2:
|
||||
return 4;
|
||||
case 1:
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (as_exit) {
|
||||
const {
|
||||
can_connect,
|
||||
can_route_ip_external_v4,
|
||||
can_route_ip_external_v6,
|
||||
can_route_ip_v4,
|
||||
can_route_ip_v6,
|
||||
} = as_exit;
|
||||
|
||||
const exitScore = [
|
||||
can_connect,
|
||||
can_route_ip_external_v4,
|
||||
can_route_ip_external_v6,
|
||||
can_route_ip_v4,
|
||||
can_route_ip_v6,
|
||||
].filter(Boolean).length;
|
||||
|
||||
switch (exitScore) {
|
||||
case 5:
|
||||
return 4;
|
||||
case 4:
|
||||
return 3;
|
||||
case 3:
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Default case if neither as_entry nor as_exit is present
|
||||
}
|
||||
|
||||
function calculateWireguardPerformance(probeResult: LastProbeResult): number {
|
||||
const calculateWireguardPerformance = (
|
||||
probeResult: LastProbeResult,
|
||||
): number => {
|
||||
const { wg, as_exit } = probeResult.outcome;
|
||||
|
||||
if (!wg) {
|
||||
@@ -155,21 +145,46 @@ function calculateWireguardPerformance(probeResult: LastProbeResult): number {
|
||||
pingPerformance <= 0.75:
|
||||
return 3;
|
||||
|
||||
case wg.can_register && (!wg.can_handshake_v4 || !wg.can_handshake_v6):
|
||||
return 2;
|
||||
|
||||
case as_exit && (!as_exit.can_route_ip_v4 || !as_exit.can_route_ip_v6):
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 1; // Default case
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const QualityIndicatorsCard = (props: IQualityIndicatorsCardProps) => {
|
||||
const { nodeInfo } = props;
|
||||
export const QualityIndicatorsCard = ({ id }: IQualityIndicatorsCardProps) => {
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nodeInfo,
|
||||
isLoading: isNodeLoading,
|
||||
isError: isNodeError,
|
||||
} = useQuery({
|
||||
queryKey: ["nodeInfo", id],
|
||||
queryFn: () => fetchNodeInfo(id),
|
||||
refetchInterval: 60000,
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const [gatewayStatus, setGatewayStatus] = useState<GatewayStatus>();
|
||||
// Fetch gateway status if nodeInfo is available
|
||||
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
|
||||
});
|
||||
|
||||
if (isNodeLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Quality indicators">
|
||||
<div>Loading...</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNodeError || !nodeInfo) {
|
||||
return (
|
||||
<ExplorerCard label="Quality indicators">
|
||||
<div>Failed to load node data.</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const nodeRoles = getNodeRoles(nodeInfo.description.declared_role);
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
@@ -179,37 +194,11 @@ export const QualityIndicatorsCard = (props: IQualityIndicatorsCardProps) => {
|
||||
));
|
||||
|
||||
const qualityOfServiceStars = nodeInfo?.uptime
|
||||
? calculateQualityOfServiceStars(nodeInfo?.uptime)
|
||||
? calculateQualityOfServiceStars(nodeInfo.uptime)
|
||||
: gatewayStatus
|
||||
? calculateQualityOfServiceStars(gatewayStatus.performance)
|
||||
: 1;
|
||||
|
||||
const nodeIsMixNodeOnly =
|
||||
NodeRoles.length === 1 && nodeRoles[0] === "Mix Node";
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch data if the node has certain roles
|
||||
if (
|
||||
nodeRoles.includes("Entry Node") ||
|
||||
nodeRoles.includes("Exit IPR Node") ||
|
||||
nodeRoles.includes("Exit NR Node")
|
||||
) {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mainnet-node-status-api.nymtech.cc/v2/gateways/${nodeInfo.identity_key}`,
|
||||
);
|
||||
const data: GatewayStatus = await response.json();
|
||||
setGatewayStatus(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}
|
||||
}, [nodeRoles, nodeInfo.identity_key]);
|
||||
|
||||
const configScoreStars = gatewayStatus
|
||||
? calculateConfigScoreStars(gatewayStatus.last_probe_result)
|
||||
: 0;
|
||||
@@ -218,8 +207,11 @@ export const QualityIndicatorsCard = (props: IQualityIndicatorsCardProps) => {
|
||||
? calculateWireguardPerformance(gatewayStatus.last_probe_result)
|
||||
: 0;
|
||||
|
||||
const nodeIsMixNodeOnly =
|
||||
NodeRoles.length === 1 && nodeRoles[0] === "Mix Node";
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Quality indicatiors" sx={{ height: "100%" }}>
|
||||
<ExplorerCard label="Quality indicators" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
|
||||
@@ -5,44 +5,74 @@ 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 { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const OriginalStakeCard = () => {
|
||||
const [origialStake, setOriginalStake] = useState(0);
|
||||
// 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();
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) return;
|
||||
|
||||
const fetchBalances = async () => {
|
||||
const data = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
const balances: ObservatoryBalance = await data.json();
|
||||
|
||||
return setOriginalStake(balances.delegated.amount);
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [address]);
|
||||
// Use React Query to fetch original stake
|
||||
const {
|
||||
data: originalStake = 0,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
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) {
|
||||
return null;
|
||||
return null; // Do not render if address is not available
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography variant="body2" color="error">
|
||||
Failed to load original stake.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{`${formatBigNum(origialStake / 1_000_000)} NYM`}
|
||||
{`${formatBigNum(originalStake / 1_000_000)} NYM`}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
"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 { useQuery } from "@tanstack/react-query";
|
||||
import StakeTable from "./StakeTable";
|
||||
|
||||
// Function to fetch epoch rewards
|
||||
async function fetchEpochRewards() {
|
||||
try {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
// 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: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching epoch rewards:", error);
|
||||
throw new Error("Failed to fetch epoch rewards data");
|
||||
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,
|
||||
stakeSaturationPoint: string,
|
||||
@@ -39,6 +40,7 @@ function getNodeSaturationPoint(
|
||||
return Number(ratio.toFixed());
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
@@ -65,23 +67,45 @@ const mappedNymNodes = (
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const StakeTableWithAction = async () => {
|
||||
try {
|
||||
// Fetch the epoch rewards data
|
||||
const epochRewardsData: ExplorerData["currentEpochRewardsData"] =
|
||||
await fetchEpochRewards();
|
||||
const StakeTableWithAction = () => {
|
||||
// Use React Query to fetch epoch rewards
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 60000, // Data is fresh for 60 seconds
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
});
|
||||
|
||||
// Fetch the Nym nodes
|
||||
const nodes = await getNymNodes();
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: getNymNodes,
|
||||
staleTime: 60000,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
|
||||
// Map the nodes with the rewards data
|
||||
const data = mappedNymNodes(nodes, epochRewardsData);
|
||||
|
||||
return <StakeTable nodes={data} />;
|
||||
} catch (error) {
|
||||
console.error("Error in StakeTableWithAction:", error);
|
||||
return <div>Error loading stake table data.</div>; // Fallback UI
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return <div>Loading stake table...</div>;
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNodesError) {
|
||||
return <div>Error loading stake table data. Please try again later.</div>;
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const data = mappedNymNodes(nymNodes, epochRewardsData);
|
||||
|
||||
return <StakeTable nodes={data} />;
|
||||
};
|
||||
|
||||
export default StakeTableWithAction;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import NextEpochTime from "@/components/epochtime/EpochTime";
|
||||
import Grid2 from "@mui/material/Grid2";
|
||||
import SubHeaderRowActions from "./SubHeaderRowActions";
|
||||
// import { QueryProvider } from "../../providers/QueryProvider";
|
||||
|
||||
const SubHeaderRow = () => {
|
||||
return (
|
||||
|
||||
@@ -1,72 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
|
||||
import type { ObservatoryBalance } from "@/app/api/types";
|
||||
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 { useCallback, useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import Loading from "../loading";
|
||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||
import RedeemRewardsModal from "../redeemRewards/RedeemRewardsModal";
|
||||
|
||||
const fee = { gas: "1000000", amount: [{ amount: "1000000", denom: "unym" }] };
|
||||
|
||||
// Fetch delegations
|
||||
const fetchDelegations = async (
|
||||
address: string,
|
||||
nymClient: any,
|
||||
): Promise<Delegation[]> => {
|
||||
const data = await nymClient.getDelegatorDelegations({ delegator: address });
|
||||
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 [delegations, setDelegations] = useState<Delegation[]>([]);
|
||||
const [totalStakerRewards, setTotalStakerRewards] = useState<number>(0);
|
||||
const [openRedeemRewardsModal, setOpenRedeemRewardsModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const { address, nymClient } = useNymClient();
|
||||
const { getSigningCosmWasmClient } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nymClient || !address) return;
|
||||
// Fetch delegations using React Query
|
||||
const {
|
||||
data: delegations = [],
|
||||
isLoading: isDelegationsLoading,
|
||||
isError: isDelegationsError,
|
||||
} = useQuery({
|
||||
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,
|
||||
});
|
||||
|
||||
const fetchDelegations = async () => {
|
||||
try {
|
||||
const data = await nymClient.getDelegatorDelegations({
|
||||
delegator: address,
|
||||
});
|
||||
setDelegations(data.delegations);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch delegations:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDelegations();
|
||||
|
||||
const fetchBalances = async () => {
|
||||
try {
|
||||
const data = await fetch(
|
||||
`${DATA_OBSERVATORY_BALANCES_URL}/${address}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
},
|
||||
);
|
||||
const balances: ObservatoryBalance = await data.json();
|
||||
|
||||
setTotalStakerRewards(balances.rewards.staking_rewards.amount);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch balances:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [address, nymClient]);
|
||||
// Fetch total rewards using React Query
|
||||
const {
|
||||
data: totalStakerRewards = 0,
|
||||
isLoading: isRewardsLoading,
|
||||
isError: isRewardsError,
|
||||
} = useQuery({
|
||||
queryKey: ["totalRewards", address],
|
||||
queryFn: () => fetchTotalRewards(address || ""),
|
||||
enabled: !!address, // Only fetch if address is available
|
||||
refetchInterval: 60000, // Refetch every 60 seconds
|
||||
staleTime: 60000,
|
||||
});
|
||||
|
||||
const handleRedeemRewards = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -77,28 +88,20 @@ const SubHeaderRowActions = () => {
|
||||
throw new Error("Wallet, client, or delegations not available.");
|
||||
}
|
||||
|
||||
const messages = delegations.map((delegation) => {
|
||||
const nodeId = delegation.node_id;
|
||||
console.log("delegations :>> ", delegations);
|
||||
|
||||
// Generate the withdraw message
|
||||
const tx = {
|
||||
contractAddress: NYM_MIXNET_CONTRACT,
|
||||
funds: [],
|
||||
msg: {
|
||||
withdraw_delegator_reward: {
|
||||
node_id: nodeId,
|
||||
},
|
||||
const messages = delegations.map((delegation: NodeRewardDetails) => ({
|
||||
contractAddress: NYM_MIXNET_CONTRACT,
|
||||
funds: [],
|
||||
msg: {
|
||||
withdraw_delegator_reward: {
|
||||
node_id: delegation.node_id,
|
||||
},
|
||||
};
|
||||
|
||||
return tx;
|
||||
});
|
||||
|
||||
console.log("Messages prepared for multi-signing:", messages);
|
||||
},
|
||||
}));
|
||||
|
||||
const cosmWasmSigningClient = await getSigningCosmWasmClient();
|
||||
|
||||
// Execute all messages in one transaction
|
||||
const result = await cosmWasmSigningClient.executeMultiple(
|
||||
address,
|
||||
messages,
|
||||
@@ -106,28 +109,22 @@ const SubHeaderRowActions = () => {
|
||||
"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 }),
|
||||
});
|
||||
setOpenRedeemRewardsModal(false);
|
||||
} catch (e) {
|
||||
console.error("Error redeeming rewards:", e);
|
||||
} catch (error) {
|
||||
console.error("Error redeeming rewards:", error);
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Error",
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: "An error occurred while redeeming rewards.",
|
||||
error instanceof Error ? error.message : "Failed to redeem rewards.",
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [address, nymClient, delegations, getSigningCosmWasmClient]);
|
||||
@@ -140,6 +137,20 @@ const SubHeaderRowActions = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDelegationsLoading || isRewardsLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (isDelegationsError || isRewardsError) {
|
||||
return (
|
||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||
<Button variant="contained" disabled>
|
||||
Error loading data
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||
<Button variant="contained" onClick={handleRedeemRewardsButtonClick}>
|
||||
@@ -148,7 +159,7 @@ const SubHeaderRowActions = () => {
|
||||
{isLoading && <Loading />}
|
||||
{openRedeemRewardsModal && (
|
||||
<RedeemRewardsModal
|
||||
onRedeem={() => handleRedeemRewards()}
|
||||
onRedeem={handleRedeemRewards}
|
||||
onClose={() => setOpenRedeemRewardsModal(false)}
|
||||
totalRewardsAmount={totalStakerRewards}
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,71 @@
|
||||
"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 { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
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 [totalStakerRewards, setTotalStakerRewards] = useState<number>(0);
|
||||
const { address } = useNymClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) return;
|
||||
|
||||
const fetchBalances = async () => {
|
||||
const data = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
const balances: ObservatoryBalance = await data.json();
|
||||
|
||||
return setTotalStakerRewards(balances.rewards.staking_rewards.amount);
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [address]);
|
||||
// Use React Query to fetch total staker rewards
|
||||
const {
|
||||
data: totalStakerRewards = 0,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
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) {
|
||||
return null;
|
||||
return null; // Do not render if address is not available
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography variant="body2" color="error">
|
||||
Failed to load total rewards.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography
|
||||
|
||||
@@ -5,39 +5,70 @@ 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 { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const TotalStakeCard = () => {
|
||||
const [totalStake, setTotalStake] = useState<number>(0);
|
||||
// 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();
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) return;
|
||||
|
||||
const fetchBalances = async () => {
|
||||
const data = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
const balances: ObservatoryBalance = await data.json();
|
||||
|
||||
return setTotalStake(
|
||||
balances.rewards.staking_rewards.amount + balances.delegated.amount,
|
||||
);
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [address]);
|
||||
// Use React Query to fetch total stake
|
||||
const {
|
||||
data: totalStake = 0,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
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) {
|
||||
return null;
|
||||
return null; // Do not render if address is not available
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography variant="body2">Loading...</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography variant="body2" color="error">
|
||||
Failed to load total stake.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import CosmosKitProvider from "./CosmosKitProvider";
|
||||
import { QueryProvider } from "./QueryProvider";
|
||||
import ThemeProvider from "./ThemeProvider";
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<CosmosKitProvider>{children}</CosmosKitProvider>
|
||||
<CosmosKitProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</CosmosKitProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user