Replace SpectreDao on NodeTable and Node page

This commit is contained in:
Yana
2025-04-16 21:06:12 +03:00
parent 1afd13d6e0
commit 8f229737a3
9 changed files with 310 additions and 198 deletions
+54 -54
View File
@@ -516,77 +516,77 @@ export type NS_NODE = {
node_id: number;
node_type: string;
original_pledge: number;
rewarding_details: {
rewarding_details?: {
cost_params: {
interval_operating_cost: {
amount: string;
denom: string;
};
profit_margin_percent: string;
};
profit_margin_percent: string;
delegates: string;
last_rewarded_epoch: number;
operator: string;
total_unit_reward: string;
unique_delegations: number;
unit_delegation: string;
};
self_description: {
} | null;
self_description?: {
authenticator: {
address: string;
};
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string;
};
location: string;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string;
ip_address: [string, string];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: {
port: number;
public_key: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string;
ip_address: [string, string];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: {
port: number;
public_key: string;
};
} | null;
total_stake: string;
uptime: number;
};
@@ -3,15 +3,23 @@
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import DOMPurify from "isomorphic-dompurify";
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
import {
fetchEpochRewards,
fetchNSApiNodes,
fetchObservatoryNodes,
} from "../../app/api";
import type {
ExplorerData,
IObservatoryNode,
NS_NODE,
} from "../../app/api/types";
import { countryName } from "../../utils/countryName";
import NodeTable from "./NodeTable";
// Utility function to calculate node saturation point
function getNodeSaturationPoint(
totalStake: number,
stakeSaturationPoint: string,
stakeSaturationPoint: string
): number {
const saturation = Number.parseFloat(stakeSaturationPoint);
@@ -27,16 +35,16 @@ function getNodeSaturationPoint(
// Map nodes with rewards data
const mappedNymNodes = (
nodes: IObservatoryNode[],
epochRewardsData: ExplorerData["currentEpochRewardsData"],
epochRewardsData: ExplorerData["currentEpochRewardsData"]
) =>
nodes.map((node) => {
const nodeSaturationPoint = getNodeSaturationPoint(
node.total_stake,
epochRewardsData.interval.stake_saturation_point,
epochRewardsData.interval.stake_saturation_point
);
const cleanMoniker = DOMPurify.sanitize(
node.self_description.moniker,
node.self_description.moniker
).replace(/&/g, "&");
return {
@@ -57,6 +65,36 @@ const mappedNymNodes = (
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
export type MappedNymNode = MappedNymNodes[0];
const mappedNSApiNodes = (
nodes: NS_NODE[],
epochRewardsData: ExplorerData["currentEpochRewardsData"]
) =>
nodes.map((node) => {
const nodeSaturationPoint = getNodeSaturationPoint(
+node.total_stake,
epochRewardsData.interval.stake_saturation_point
);
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
/&amp;/g,
"&"
);
return {
name: cleanMoniker,
nodeId: node.node_id,
identity_key: node.identity_key,
countryCode: node.geoip.country || null,
countryName: countryName(node.geoip.country) || null,
profitMarginPercentage: node.rewarding_details
? +node.rewarding_details.cost_params.profit_margin_percent * 100
: 0,
owner: node.bonding_address,
stakeSaturation: nodeSaturationPoint,
qualityOfService: +node.uptime * 100,
};
});
const NodeTableWithAction = () => {
// Use React Query to fetch epoch rewards
const {
@@ -86,8 +124,21 @@ const NodeTableWithAction = () => {
refetchOnMount: false,
});
const {
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
// Handle loading state
if (isEpochLoading || isNodesLoading) {
if (isEpochLoading || isNodesLoading || isNSApiNodesLoading) {
return (
<Card sx={{ height: "100%", mt: 5 }}>
<CardContent>
@@ -101,7 +152,7 @@ const NodeTableWithAction = () => {
}
// Handle error state
if (isEpochError || isNodesError) {
if (isEpochError || isNodesError || isNSApiNodesError) {
return (
<Stack direction="row" spacing={1}>
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
@@ -119,7 +170,8 @@ const NodeTableWithAction = () => {
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
return <NodeTable nodes={data} />;
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
return <NodeTable nodes={nsApiNodesData} />;
};
export default NodeTableWithAction;
@@ -1,9 +1,9 @@
"use client";
import type { IObservatoryNode } from "@/app/api/types";
import { Skeleton, Stack, Typography } from "@mui/material";
import type { NS_NODE } from "@/app/api/types";
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { fetchObservatoryNodes } from "../../app/api";
import { fetchNSApiNodes } from "../../app/api";
import { formatBigNum } from "../../utils/formatBigNumbers";
import ExplorerCard from "../cards/ExplorerCard";
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
@@ -14,22 +14,25 @@ type Props = {
};
export const BasicInfoCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const {
data: nymNodes,
isLoading,
isError,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
if (isLoading) {
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
if (isNSApiNodesLoading) {
return (
<ExplorerCard label="Basic info">
<Skeleton variant="text" height={90} />
@@ -42,10 +45,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
);
}
if (isError || !nymNodes) {
if (!nsApiNodes || isNSApiNodesError) {
return (
<ExplorerCard label="Basic info">
<Typography variant="h3" sx={{ color: "pine.950" }}>
<Typography
variant="h3"
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
>
Failed to load node data.
</Typography>
</ExplorerCard>
@@ -55,16 +61,20 @@ export const BasicInfoCard = ({ paramId }: Props) => {
// get node info based on wether it's dentity_key or node_id
if (paramId.length > 10) {
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
if (!nodeInfo) return null;
const selfBond = formatBigNum(
Number(nodeInfo.rewarding_details.operator) / 1_000_000,
);
const selfBond = nodeInfo.rewarding_details
? formatBigNum(Number(nodeInfo.rewarding_details.operator) / 1_000_000)
: 0;
const selfBondFormatted = `${selfBond} NYM`;
return (
@@ -85,9 +95,13 @@ export const BasicInfoCard = ({ paramId }: Props) => {
variant="body4"
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
>
{nodeInfo.bonding_address}
{nodeInfo.bonding_address
? nodeInfo.bonding_address
: "Node not bonded"}
</Typography>
<CopyToClipboard text={nodeInfo.bonding_address} />
{nodeInfo.bonding_address && (
<CopyToClipboard text={nodeInfo.bonding_address} />
)}
</Stack>
}
/>
@@ -113,12 +127,14 @@ export const BasicInfoCard = ({ paramId }: Props) => {
}
/>
<ExplorerListItem
row
divider
label="Nr. of stakers"
value={nodeInfo.rewarding_details.unique_delegations.toString()}
/>
{nodeInfo.rewarding_details && (
<ExplorerListItem
row
divider
label="Nr. of stakers"
value={nodeInfo.rewarding_details.unique_delegations.toString()}
/>
)}
<ExplorerListItem row label="Self bonded" value={selfBondFormatted} />
</Stack>
</ExplorerCard>
@@ -1,10 +1,10 @@
"use client";
import type { IObservatoryNode } from "@/app/api/types";
import { Skeleton, Typography } from "@mui/material";
import type { NS_NODE } from "@/app/api/types";
import { Skeleton, Typography, useTheme } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { format } from "date-fns";
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
import ExplorerCard from "../cards/ExplorerCard";
import ExplorerListItem from "../list/ListItem";
@@ -13,7 +13,7 @@ type Props = {
};
export const NodeDataCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const {
data: epochRewardsData,
@@ -30,19 +30,22 @@ export const NodeDataCard = ({ paramId }: Props) => {
// Fetch node information
const {
data: nymNodes,
isLoading,
isError,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
if (isEpochLoading || isLoading) {
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
if (isEpochLoading || isNSApiNodesLoading) {
return (
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
<Skeleton variant="text" height={50} />
@@ -53,10 +56,13 @@ export const NodeDataCard = ({ paramId }: Props) => {
);
}
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
return (
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
<Typography variant="h3" sx={{ color: "pine.950" }}>
<Typography
variant="h3"
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
>
Failed to load node data.
</Typography>
</ExplorerCard>
@@ -66,17 +72,23 @@ export const NodeDataCard = ({ paramId }: Props) => {
// get node info based on wether it's dentity_key or node_id
if (paramId.length > 10) {
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
if (!nodeInfo) return null;
const softwareUpdateTime = format(
new Date(nodeInfo.description.build_information.build_timestamp),
"dd/MM/yyyy",
);
const softwareUpdateTime = nodeInfo.self_description
? format(
new Date(nodeInfo.self_description.build_information.build_timestamp),
"dd/MM/yyyy"
)
: "N/A";
return (
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
@@ -90,13 +102,21 @@ export const NodeDataCard = ({ paramId }: Props) => {
row
divider
label="Host"
value={nodeInfo.description.host_information.ip_address.toString()}
value={
nodeInfo.self_description
? nodeInfo.self_description.host_information.ip_address.toString()
: "N/A"
}
/>
<ExplorerListItem
row
divider
label="Version"
value={nodeInfo.description.build_information.build_version}
value={
nodeInfo.self_description
? nodeInfo.self_description.build_information.build_version
: "N/A"
}
/>
<ExplorerListItem
row
@@ -1,7 +1,7 @@
"use client";
import { fetchObservatoryNodes } from "@/app/api";
import type { IObservatoryNode } from "@/app/api/types";
import { fetchNSApiNodes } from "@/app/api";
import type { NS_NODE } from "@/app/api/types";
import { Skeleton, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import ExplorerCard from "../cards/ExplorerCard";
@@ -12,15 +12,15 @@ type Props = {
};
const NodeDelegationsCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const {
data: nymNodes,
isError,
isLoading,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
@@ -28,16 +28,20 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
});
if (paramId.length > 10) {
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
if (!nodeInfo) return null;
const id = nodeInfo.node_id;
if (isLoading) {
if (isNSApiNodesLoading) {
return (
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
<Skeleton variant="text" height={50} />
@@ -48,7 +52,7 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
);
}
if (isError) {
if (isNSApiNodesError) {
return (
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
<Typography variant="h3" sx={{ color: "pine.950" }}>
@@ -1,7 +1,7 @@
"use client";
import { fetchObservatoryNodes } from "@/app/api";
import type { IObservatoryNode } from "@/app/api/types";
import { fetchNSApiNodes } from "@/app/api";
import type { NS_NODE } from "@/app/api/types";
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
import { useQuery } from "@tanstack/react-query";
@@ -10,27 +10,32 @@ type Props = {
};
export default function NodePageButtonGroup({ paramId }: Props) {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const { data: nymNodes, isError } = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
if (!nymNodes || isError) return null;
if (!nsApiNodes || isNSApiNodesError) return null;
// get node info based on wether it's dentity_key or node_id
if (paramId.length > 10) {
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
if (!nodeInfo) return null;
if (nodeInfo.bonding_address)
@@ -3,8 +3,8 @@
import { formatBigNum } from "@/utils/formatBigNumbers";
import { Skeleton, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
import type { IObservatoryNode, RewardingDetails } from "../../app/api/types";
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
import type { NS_NODE } from "../../app/api/types";
import ExplorerCard from "../cards/ExplorerCard";
import ExplorerListItem from "../list/ListItem";
@@ -13,7 +13,7 @@ type Props = {
};
export const NodeParametersCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
// Fetch epoch rewards
const {
@@ -31,19 +31,19 @@ export const NodeParametersCard = ({ paramId }: Props) => {
// Fetch node information
const {
data: nymNodes,
isLoading,
isError,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
if (isEpochLoading || isLoading) {
if (isEpochLoading || isNSApiNodesLoading) {
return (
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
<Skeleton variant="text" height={50} />
@@ -54,7 +54,7 @@ export const NodeParametersCard = ({ paramId }: Props) => {
);
}
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
return (
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
<Typography variant="h3" sx={{ color: "pine.950" }}>
@@ -66,9 +66,13 @@ export const NodeParametersCard = ({ paramId }: Props) => {
// get node info based on wether it's dentity_key or node_id
if (paramId.length > 10) {
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
if (!nodeInfo) return null;
@@ -77,22 +81,25 @@ export const NodeParametersCard = ({ paramId }: Props) => {
const totalStakeFormatted = `${totalStake} NYM`;
// Extract reward details
const rewardDetails: RewardingDetails = nodeInfo.rewarding_details;
const profitMarginPercent =
Number(rewardDetails.cost_params.profit_margin_percent) * 100;
const profitMarginPercent = nodeInfo.rewarding_details
? Number(nodeInfo.rewarding_details.cost_params.profit_margin_percent) * 100
: 0;
const profitMarginPercentFormated = `${profitMarginPercent}%`;
const operatingCosts =
Number(rewardDetails.cost_params.interval_operating_cost.amount) /
1_000_000;
const operatingCosts = nodeInfo.rewarding_details
? Number(
nodeInfo.rewarding_details.cost_params.interval_operating_cost.amount
) / 1_000_000
: 0;
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
const getNodeSaturationPoint = (
totalStake: number,
stakeSaturationPoint: string,
nodeTotalStake: string,
stakeSaturationPoint: string
): string => {
const saturation = Number.parseFloat(stakeSaturationPoint);
const totalStake = Number.parseFloat(nodeTotalStake);
if (Number.isNaN(saturation) || saturation <= 0) {
throw new Error("Invalid stake saturation point provided");
@@ -105,7 +112,7 @@ export const NodeParametersCard = ({ paramId }: Props) => {
const nodeSaturationPoint = getNodeSaturationPoint(
nodeInfo.total_stake,
epochRewardsData.interval.stake_saturation_point,
epochRewardsData.interval.stake_saturation_point
);
return (
@@ -1,6 +1,6 @@
"use client";
import type { IObservatoryNode } from "@/app/api/types";
import type { NS_NODE } from "@/app/api/types";
import { useChain } from "@cosmos-kit/react";
import {
Box,
@@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
import DOMPurify from "isomorphic-dompurify";
import { useCallback, useState } from "react";
import { RandomAvatar } from "react-random-avatars";
import { fetchObservatoryNodes } from "../../app/api";
import { fetchNSApiNodes } from "../../app/api";
import { COSMOS_KIT_USE_CHAIN } from "../../config";
import { useNymClient } from "../../hooks/useNymClient";
import ExplorerCard from "../cards/ExplorerCard";
@@ -31,7 +31,7 @@ type Props = {
};
export const NodeProfileCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const theme = useTheme();
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
@@ -47,12 +47,12 @@ export const NodeProfileCard = ({ paramId }: Props) => {
// Fetch node info
const {
data: nymNodes,
isLoading: isLoadingNymNodes,
isError,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
@@ -62,9 +62,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
// get node info based on wether it's dentity_key or node_id
if (paramId.length > 10) {
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.identity_key === paramId
);
} else {
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find(
(node: NS_NODE) => node.node_id === Number(paramId)
);
}
const handleOnSelectStake = useCallback(() => {
@@ -96,7 +100,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
}
}, [isWalletConnected, nodeInfo]);
if (isLoadingNymNodes) {
if (isNSApiNodesLoading) {
return (
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
<Skeleton variant="rectangular" height={80} width={80} />
@@ -105,7 +109,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
</ExplorerCard>
);
}
if (isError || !nymNodes) {
if (isNSApiNodesError || !nsApiNodes) {
return (
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
<Typography
@@ -137,7 +141,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
{ nodeId },
fee,
"Delegation from Nym Explorer V2",
uNymFunds,
uNymFunds
);
setSelectedNodeForStaking(undefined);
setInfoModalProps({
@@ -164,12 +168,13 @@ export const NodeProfileCard = ({ paramId }: Props) => {
if (!nodeInfo) return null;
const cleanMoniker = DOMPurify.sanitize(
nodeInfo?.self_description.moniker,
).replace(/&amp;/g, "&");
const cleanMoniker = DOMPurify.sanitize(nodeInfo.description.moniker).replace(
/&amp;/g,
"&"
);
const cleanDescription = DOMPurify.sanitize(
nodeInfo?.self_description.details,
nodeInfo.description.details
).replace(/&amp;/g, "&");
// get full country name
@@ -197,7 +202,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
>
{cleanMoniker || "Moniker"}
</Typography>
{nodeInfo.description.auxiliary_details.location && (
{nodeInfo.geoip.country && (
<Box display={"flex"} gap={1}>
<Typography
variant="h6"
@@ -210,10 +215,8 @@ export const NodeProfileCard = ({ paramId }: Props) => {
<Box>
<CountryFlag
countryCode={nodeInfo.description.auxiliary_details.location}
countryName={countryName(
nodeInfo.description.auxiliary_details.location,
)}
countryCode={nodeInfo.geoip.country}
countryName={countryName(nodeInfo.geoip.country)}
/>
</Box>
</Box>
@@ -1,16 +1,16 @@
"use client";
import { Chip, Skeleton, Stack, Typography } from "@mui/material";
import { Chip, Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import {
fetchEpochRewards,
fetchGatewayStatus,
fetchObservatoryNodes,
fetchNSApiNodes,
} from "../../app/api";
import type {
IObservatoryNode,
LastProbeResult,
NodeDescription,
NS_NODE,
} from "../../app/api/types";
import ExplorerCard from "../cards/ExplorerCard";
import ExplorerListItem from "../list/ListItem";
@@ -32,7 +32,7 @@ const roleMapping: Record<DeclaredRoleKey, RoleString> = {
};
const getNodeRoles = (
declaredRoles: NodeDescriptionNotNull["declared_role"],
declaredRoles: NodeDescriptionNotNull["declared_role"]
): RoleString[] => {
return Object.entries(declaredRoles)
.filter(([, isActive]) => isActive)
@@ -81,7 +81,7 @@ function calculateConfigScoreStars(probeResult: LastProbeResult): number {
if (as_entry) {
const entryScore = [as_entry.can_connect, as_entry.can_route].filter(
Boolean,
Boolean
).length;
return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1;
@@ -148,21 +148,23 @@ function calculateWireguardPerformance(probeResult: LastProbeResult): number {
}
export const NodeRoleCard = ({ paramId }: Props) => {
let nodeInfo: IObservatoryNode | undefined;
let nodeInfo: NS_NODE | undefined;
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
// Fetch node info
const {
data: nymNodes,
isLoading,
isError,
data: nsApiNodes = [],
isLoading: isNSApiNodesLoading,
isError: isNSApiNodesError,
} = useQuery({
queryKey: ["nymNodes"],
queryFn: fetchObservatoryNodes,
queryKey: ["nsApiNodes"],
queryFn: fetchNSApiNodes,
staleTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false, // Prevents unnecessary refetching
refetchOnReconnect: false,
refetchOnMount: false,
});
const {
data: epochRewardsData,
isLoading: isEpochLoading,
@@ -177,19 +179,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
});
if (paramId.length > 10) {
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
nodeInfo = nsApiNodes.find((node) => node.identity_key === paramId);
} else {
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
nodeInfo = nsApiNodes.find((node) => node.node_id === Number(paramId));
} // Extract node roles once `nodeInfo` is available
const nodeRoles = nodeInfo
? getNodeRoles(nodeInfo.description.declared_role)
const nodeRoles = nodeInfo?.self_description
? getNodeRoles(nodeInfo.self_description.declared_role)
: [];
// Define whether to fetch gateway status
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role)
);
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
const { data: gatewayStatus } = useQuery({
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
@@ -200,7 +202,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
refetchOnReconnect: false,
});
if (isLoading || isEpochLoading) {
if (isNSApiNodesLoading || isEpochLoading) {
return (
<ExplorerCard label="Node role & performance">
<Skeleton variant="text" height={70} />
@@ -210,15 +212,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
);
}
if (isError || !nymNodes || !epochRewardsData || isEpochError) {
if (isNSApiNodesError || !nsApiNodes || !epochRewardsData || isEpochError) {
return (
<ExplorerCard label="Node role & performance">
<Typography variant="h3" sx={{ color: "pine.950" }}>
<Typography
variant="h3"
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
>
Failed to load node data.
</Typography>
</ExplorerCard>
);
}
if (!nodeInfo) return null;
const NodeRoles = nodeRoles.map((role) => (
<Stack key={role} direction="row" gap={1}>
@@ -226,8 +232,6 @@ export const NodeRoleCard = ({ paramId }: Props) => {
</Stack>
));
if (!nodeInfo) return null;
const qualityOfServiceStars = nodeInfo?.uptime
? calculateQualityOfServiceStars(nodeInfo.uptime)
: gatewayStatus
@@ -247,9 +251,10 @@ export const NodeRoleCard = ({ paramId }: Props) => {
// Function to calculate active set probability
const getActiveSetProbability = (
totalStake: number,
stakeSaturationPoint: string,
nodeTotalStake: string,
stakeSaturationPoint: string
): string => {
const totalStake = Number.parseFloat(nodeTotalStake);
const saturation = Number.parseFloat(stakeSaturationPoint);
if (Number.isNaN(saturation) || saturation <= 0) {
@@ -268,7 +273,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
};
const activeSetProb = getActiveSetProbability(
nodeInfo.total_stake,
epochRewardsData.interval.stake_saturation_point,
epochRewardsData.interval.stake_saturation_point
);
return (