From bd79fb12005803cd8cc4b8bb6c7806be15ac43a5 Mon Sep 17 00:00:00 2001 From: Yana Date: Thu, 23 Jan 2025 21:03:35 +0200 Subject: [PATCH] Add tanstack react-query for refetching data on epoch change --- .../src/app/(pages)/nym-node/[id]/page.tsx | 144 +++++--------- explorer-nextjs/src/app/api/index.tsx | 50 +++++ explorer-nextjs/src/app/page.tsx | 7 +- .../src/components/epochtime/EpochTime.tsx | 57 +++++- .../CurrentEpochCard.tsx | 4 +- .../nodeTable/NodeTableWithAction.tsx | 87 +++++--- .../nymNodePageComponents/BasicInfoCard.tsx | 86 ++++++-- .../DelegationsTable.tsx | 0 .../NodeDelegationsCard.tsx | 59 ++++++ .../nymNodePageComponents/NodeMetricsCard.tsx | 134 ++++++++++--- .../nymNodePageComponents/NodeProfileCard.tsx | 65 +++++- .../nymNodePageComponents/NodeRewardsCard.tsx | 137 ++++++++++--- .../QualityIndicatorsCard.tsx | 188 +++++++++--------- .../components/staking/OriginalStakeCard.tsx | 78 +++++--- .../staking/StakeTableWithAction.tsx | 88 +++++--- .../src/components/staking/SubHeaderRow.tsx | 1 + .../staking/SubHeaderRowActions.tsx | 157 ++++++++------- .../components/staking/TotalRewardsCard.tsx | 77 +++++-- .../src/components/staking/TotalStakeCard.tsx | 81 +++++--- .../QueryProvider.tsx | 0 explorer-nextjs/src/providers/index.tsx | 5 +- 21 files changed, 997 insertions(+), 508 deletions(-) rename explorer-nextjs/src/components/{nodeTable => nymNodePageComponents}/DelegationsTable.tsx (100%) create mode 100644 explorer-nextjs/src/components/nymNodePageComponents/NodeDelegationsCard.tsx rename explorer-nextjs/src/{components/queryProvider => providers}/QueryProvider.tsx (100%) diff --git a/explorer-nextjs/src/app/(pages)/nym-node/[id]/page.tsx b/explorer-nextjs/src/app/(pages)/nym-node/[id]/page.tsx index 9c5bb26058..f2f67129bc 100644 --- a/explorer-nextjs/src/app/(pages)/nym-node/[id]/page.tsx +++ b/explorer-nextjs/src/app/(pages)/nym-node/[id]/page.tsx @@ -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 ( @@ -98,77 +76,53 @@ export default async function NymNode({ )} - {observatoryNymNode && ( - - - - )} - {observatoryNymNode.rewarding_details && ( - - - - )} - {observatoryNymNode && ( - - - - )} - {observatoryNymNode.rewarding_details && ( - - - - )} - {observatoryNymNode && ( - - - - )} - {delegations && ( - - - - - - )} + + + + + + + + + + + + + + + + + + { + 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(); +}; diff --git a/explorer-nextjs/src/app/page.tsx b/explorer-nextjs/src/app/page.tsx index 82e35fdc20..91847493d5 100644 --- a/explorer-nextjs/src/app/page.tsx +++ b/explorer-nextjs/src/app/page.tsx @@ -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() { }> - - - + diff --git a/explorer-nextjs/src/components/epochtime/EpochTime.tsx b/explorer-nextjs/src/components/epochtime/EpochTime.tsx index ef2ef8149e..33b647f896 100644 --- a/explorer-nextjs/src/components/epochtime/EpochTime.tsx +++ b/explorer-nextjs/src/components/epochtime/EpochTime.tsx @@ -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 ( - Next epoch: {formattedDate} + Loading next epoch... ); - } catch (error) { - console.log(error); - return null; } + + if (isError || !data) { + return ( + + + + Failed to load next epoch. + + + ); + } + + const formattedDate = format(data.dateTime, "HH:mm:ss"); + + return ( + + + + Next epoch: {formattedDate} + + + ); }; export default NextEpochTime; diff --git a/explorer-nextjs/src/components/landingPageComponents/CurrentEpochCard.tsx b/explorer-nextjs/src/components/landingPageComponents/CurrentEpochCard.tsx index 779bb74025..3a1d9dbe30 100644 --- a/explorer-nextjs/src/components/landingPageComponents/CurrentEpochCard.tsx +++ b/explorer-nextjs/src/components/landingPageComponents/CurrentEpochCard.tsx @@ -16,9 +16,7 @@ const fetchCurrentEpoch = async (): Promise => { }); 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(); diff --git a/explorer-nextjs/src/components/nodeTable/NodeTableWithAction.tsx b/explorer-nextjs/src/components/nodeTable/NodeTableWithAction.tsx index 86ef6a7e40..348904c481 100644 --- a/explorer-nextjs/src/components/nodeTable/NodeTableWithAction.tsx +++ b/explorer-nextjs/src/components/nodeTable/NodeTableWithAction.tsx @@ -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; 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 ; - } catch (error) { - console.error("Error in NodeTableWithAction:", error); - return
Error loading data.
; // Render error fallback UI + // Handle loading state + if (isEpochLoading || isNodesLoading) { + return
Loading...
; } + + // Handle error state + if (isEpochError || isNodesError) { + return
Error loading data. Please try again later.
; + } + + // Map nodes with rewards data + const data = mappedNymNodes(nymNodes, epochRewardsData); + + return ; }; export default NodeTableWithAction; diff --git a/explorer-nextjs/src/components/nymNodePageComponents/BasicInfoCard.tsx b/explorer-nextjs/src/components/nymNodePageComponents/BasicInfoCard.tsx index bfe63f1fb3..4fe07b3723 100644 --- a/explorer-nextjs/src/components/nymNodePageComponents/BasicInfoCard.tsx +++ b/explorer-nextjs/src/components/nymNodePageComponents/BasicInfoCard.tsx @@ -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 => { + 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 ( + + Loading... + + ); + } + + // Error state + if (isError || !nodeInfo) { + return ( + + Failed to load node information. + + ); + } + + // 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 ( @@ -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()} /> - + ); diff --git a/explorer-nextjs/src/components/nodeTable/DelegationsTable.tsx b/explorer-nextjs/src/components/nymNodePageComponents/DelegationsTable.tsx similarity index 100% rename from explorer-nextjs/src/components/nodeTable/DelegationsTable.tsx rename to explorer-nextjs/src/components/nymNodePageComponents/DelegationsTable.tsx diff --git a/explorer-nextjs/src/components/nymNodePageComponents/NodeDelegationsCard.tsx b/explorer-nextjs/src/components/nymNodePageComponents/NodeDelegationsCard.tsx new file mode 100644 index 0000000000..1d97cf81fd --- /dev/null +++ b/explorer-nextjs/src/components/nymNodePageComponents/NodeDelegationsCard.tsx @@ -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 => { + 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 ( + + {isLoading &&
Loading delegations...
} + {isError && ( +
Failed to load delegations. Please try again later.
+ )} + {!isLoading && !isError && } +
+ ); +}; + +export default NodeDelegationsCard; diff --git a/explorer-nextjs/src/components/nymNodePageComponents/NodeMetricsCard.tsx b/explorer-nextjs/src/components/nymNodePageComponents/NodeMetricsCard.tsx index 8f85c2aef5..491ad63e43 100644 --- a/explorer-nextjs/src/components/nymNodePageComponents/NodeMetricsCard.tsx +++ b/explorer-nextjs/src/components/nymNodePageComponents/NodeMetricsCard.tsx @@ -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 => { + 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 ( + +
Loading...
+
+ ); + } + + if (isEpochError || isNodeError || !nodeInfo || !epochRewardsData) { + return ( + +
Failed to load data
+
+ ); + } + + // 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 ( @@ -48,23 +124,19 @@ export const NodeMetricsCard = async (props: INodeMetricsCardProps) => { label="Node ID." value={nodeInfo.node_id.toString()} /> - <> - - - - {epochRewardsData && ( - - )} + + + ); }; diff --git a/explorer-nextjs/src/components/nymNodePageComponents/NodeProfileCard.tsx b/explorer-nextjs/src/components/nymNodePageComponents/NodeProfileCard.tsx index 9be4f938bf..82486e3511 100644 --- a/explorer-nextjs/src/components/nymNodePageComponents/NodeProfileCard.tsx +++ b/explorer-nextjs/src/components/nymNodePageComponents/NodeProfileCard.tsx @@ -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({ @@ -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 ( + +
Loading...
+
+ ); + } + + if (isNodeError || !nodeInfo) { + return ( + +
Failed to load node information.
+
+ ); + } + return ( diff --git a/explorer-nextjs/src/components/nymNodePageComponents/NodeRewardsCard.tsx b/explorer-nextjs/src/components/nymNodePageComponents/NodeRewardsCard.tsx index c8aac9fffc..4c4c29ef74 100644 --- a/explorer-nextjs/src/components/nymNodePageComponents/NodeRewardsCard.tsx +++ b/explorer-nextjs/src/components/nymNodePageComponents/NodeRewardsCard.tsx @@ -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 => { + 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 ( + +
Loading...
+
+ ); + } + + if (isEpochError || isNodeError || !nodeInfo || !epochRewardsData) { + return ( + +
Failed to load data
+
+ ); + } + + // 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 ( - - {/* */} + - {/* */} ; -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 = { +const roleMapping: Record = { 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 => { + 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 => { + 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(); + // 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 ( + +
Loading...
+
+ ); + } + + if (isNodeError || !nodeInfo) { + return ( + +
Failed to load node data.
+
+ ); + } 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 ( - + { - const [origialStake, setOriginalStake] = useState(0); +// Fetch function to get the original stake +const fetchOriginalStake = async (address: string): Promise => { + 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 ( + + Loading... + + ); + } + + if (isError) { + return ( + + + Failed to load original stake. + + + ); + } + return ( - {`${formatBigNum(origialStake / 1_000_000)} NYM`} + {`${formatBigNum(originalStake / 1_000_000)} NYM`} ); diff --git a/explorer-nextjs/src/components/staking/StakeTableWithAction.tsx b/explorer-nextjs/src/components/staking/StakeTableWithAction.tsx index 5f128513e5..33b295a350 100644 --- a/explorer-nextjs/src/components/staking/StakeTableWithAction.tsx +++ b/explorer-nextjs/src/components/staking/StakeTableWithAction.tsx @@ -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; 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 ; - } catch (error) { - console.error("Error in StakeTableWithAction:", error); - return
Error loading stake table data.
; // Fallback UI + // Handle loading state + if (isEpochLoading || isNodesLoading) { + return
Loading stake table...
; } + + // Handle error state + if (isEpochError || isNodesError) { + return
Error loading stake table data. Please try again later.
; + } + + // Map nodes with rewards data + const data = mappedNymNodes(nymNodes, epochRewardsData); + + return ; }; export default StakeTableWithAction; diff --git a/explorer-nextjs/src/components/staking/SubHeaderRow.tsx b/explorer-nextjs/src/components/staking/SubHeaderRow.tsx index 01be6d4490..b90731f4bc 100644 --- a/explorer-nextjs/src/components/staking/SubHeaderRow.tsx +++ b/explorer-nextjs/src/components/staking/SubHeaderRow.tsx @@ -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 ( diff --git a/explorer-nextjs/src/components/staking/SubHeaderRowActions.tsx b/explorer-nextjs/src/components/staking/SubHeaderRowActions.tsx index ed635ae472..a9d49237d9 100644 --- a/explorer-nextjs/src/components/staking/SubHeaderRowActions.tsx +++ b/explorer-nextjs/src/components/staking/SubHeaderRowActions.tsx @@ -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 => { + const data = await nymClient.getDelegatorDelegations({ delegator: address }); + return data.delegations; +}; + +// Fetch total staker rewards +const fetchTotalRewards = async (address: string): Promise => { + 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([]); - const [totalStakerRewards, setTotalStakerRewards] = useState(0); const [openRedeemRewardsModal, setOpenRedeemRewardsModal] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [infoModalProps, setInfoModalProps] = useState({ 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 ; + } + + if (isDelegationsError || isRewardsError) { + return ( + + + + ); + } + return (