Replace SpectreDao on NodeTable and Node page
This commit is contained in:
@@ -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(
|
||||
/&/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(/&/g, "&");
|
||||
const cleanMoniker = DOMPurify.sanitize(nodeInfo.description.moniker).replace(
|
||||
/&/g,
|
||||
"&"
|
||||
);
|
||||
|
||||
const cleanDescription = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.details,
|
||||
nodeInfo.description.details
|
||||
).replace(/&/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 (
|
||||
|
||||
Reference in New Issue
Block a user