Add tanstack react-query for refetching data on epoch change

This commit is contained in:
Yana
2025-01-23 21:03:35 +02:00
parent 11a3bee257
commit bd79fb1200
21 changed files with 997 additions and 508 deletions
@@ -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={{
+50
View File
@@ -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();
};
+2 -5
View File
@@ -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
+4 -1
View File
@@ -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>
);
};