diff --git a/explorer-v2/src/app/api/index.tsx b/explorer-v2/src/app/api/index.tsx
index e56faebe7a..27a8ba84fe 100644
--- a/explorer-v2/src/app/api/index.tsx
+++ b/explorer-v2/src/app/api/index.tsx
@@ -277,6 +277,9 @@ export const fetchWorldMapCountries =
// Increment the count for this country
countryCounts[threeLetterCode] =
(countryCounts[threeLetterCode] || 0) + 1;
+ } else {
+ // If no geoip data, count it as unknown
+ countryCounts[""] = (countryCounts[""] || 0) + 1;
}
}
diff --git a/explorer-v2/src/app/api/types.ts b/explorer-v2/src/app/api/types.ts
index 5d474233bd..c758243344 100644
--- a/explorer-v2/src/app/api/types.ts
+++ b/explorer-v2/src/app/api/types.ts
@@ -72,56 +72,59 @@ export interface ExplorerData {
}
export type NodeDescription = {
- last_polled: string;
+ authenticator: {
+ address: string;
+ };
+ auxiliary_details: {
+ accepted_operator_terms_and_conditions: boolean;
+ announce_ports: {
+ mix_port: number | null;
+ verloc_port: number | null;
+ };
+ 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: {
- ip_address: string[];
hostname: string;
+ ip_address: [string, string];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
- declared_role: {
- mixnode: boolean;
- entry: boolean;
- exit_nr: boolean;
- exit_ipr: boolean;
+ ip_packet_router: {
+ address: string;
};
- auxiliary_details: {
- location: string;
- announce_ports: {
- verloc_port: number | null;
- mix_port: number | null;
- };
- accepted_operator_terms_and_conditions: boolean;
- };
- build_information: {
- binary_name: string;
- build_timestamp: string;
- build_version: string;
- commit_sha: string;
- commit_timestamp: string;
- commit_branch: string;
- rustc_version: string;
- rustc_channel: string;
- cargo_profile: string;
- cargo_triple: string;
+ last_polled: string;
+ mixnet_websockets: {
+ ws_port: number;
+ wss_port: number;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
- ip_packet_router: {
- address: string;
- };
- authenticator: {
- address: string;
- };
- wireguard: string | null;
- mixnet_websockets: {
- ws_port: number;
- wss_port: number | null;
+ wireguard: {
+ port: number;
+ public_key: string;
};
} | null;
@@ -165,15 +168,6 @@ export type Location = {
longitude?: number;
};
-export type NodeData = {
- node_id: number;
- contract_node_type: string;
- description: NodeDescription;
- bond_information: BondInformation;
- rewarding_details: RewardingDetails;
- location: Location;
-};
-
// ACCOUNT BALANCES
export interface IRewardDetails {
@@ -207,7 +201,6 @@ export interface IAccountBalancesInfo {
vesting_account?: null | string;
}
-
export interface NodeRewardDetails {
amount: {
amount: string;
@@ -405,7 +398,7 @@ export type NS_NODE = {
security_contact: string;
website: string;
};
- geoip: {
+ geoip?: {
city: string;
country: string;
ip_address: string;
@@ -436,62 +429,7 @@ export type NS_NODE = {
unique_delegations: number;
unit_delegation: string;
} | null;
- self_description?: {
- authenticator: {
- address: string;
- };
- auxiliary_details: {
- accepted_operator_terms_and_conditions: boolean;
- announce_ports: {
- mix_port: number | null;
- verloc_port: number | null;
- };
- 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;
- };
- };
- 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;
+ self_description?: NodeDescription;
total_stake: string;
uptime: number;
};
diff --git a/explorer-v2/src/app/page.tsx b/explorer-v2/src/app/page.tsx
index db86757dd9..fca5b792bc 100644
--- a/explorer-v2/src/app/page.tsx
+++ b/explorer-v2/src/app/page.tsx
@@ -47,7 +47,7 @@ export default async function Home() {
-
+
diff --git a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx
index f9d2e060e1..62a065ea1a 100644
--- a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx
+++ b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx
@@ -7,6 +7,8 @@ import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
import type { ExplorerData, NS_NODE } from "../../app/api/types";
import { countryName } from "../../utils/countryName";
import NodeTable from "./NodeTable";
+import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup";
+import { useState } from "react";
// Utility function to calculate node saturation point
function getNodeSaturationPoint(
@@ -45,21 +47,30 @@ const mappedNSApiNodes = (
name: cleanMoniker,
nodeId: node.node_id,
identity_key: node.identity_key,
- countryCode: node.geoip.country || null,
- countryName: countryName(node.geoip.country) || null,
+ countryCode: node.geoip?.country || null,
+ countryName: countryName(node.geoip?.country || null) || 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,
+ mixnode: node.self_description?.declared_role.mixnode === true,
+ gateway:
+ node.self_description?.declared_role.entry === true ||
+ node.self_description?.declared_role.exit_ipr === true ||
+ node.self_description?.declared_role.exit_nr === true,
};
});
- export type MappedNymNodes = ReturnType;
- export type MappedNymNode = MappedNymNodes[0];
+export type MappedNymNodes = ReturnType;
+export type MappedNymNode = MappedNymNodes[0];
const NodeTableWithAction = () => {
+ const [activeFilter, setActiveFilter] = useState<
+ "all" | "mixnodes" | "gateways"
+ >("all");
+
// Use React Query to fetch epoch rewards
const {
data: epochRewardsData,
@@ -115,13 +126,48 @@ const NodeTableWithAction = () => {
}
// Map nodes with rewards data
-
if (!epochRewardsData) {
return null;
}
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
- return ;
+
+ // Filter nodes based on active filter
+ const filteredNodes = nsApiNodesData.filter((node) => {
+ switch (activeFilter) {
+ case "mixnodes":
+ return node.mixnode;
+ case "gateways":
+ return node.gateway;
+ default:
+ return true;
+ }
+ });
+
+ return (
+
+
+
+
+ );
};
export default NodeTableWithAction;
diff --git a/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx b/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx
index 51256dc21c..21c89ba5e8 100644
--- a/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx
+++ b/explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx
@@ -202,7 +202,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
>
{cleanMoniker || "Moniker"}
- {nodeInfo.geoip.country && (
+ {nodeInfo.geoip?.country && (
{
diff --git a/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx b/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx
index 18431d5731..7cac5cd0fb 100644
--- a/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx
+++ b/explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx
@@ -226,6 +226,8 @@ export const NodeRoleCard = ({ paramId }: Props) => {
}
if (!nodeInfo) return null;
+ console.log("nodeInfo", nodeInfo);
+
const NodeRoles = nodeRoles.map((role) => (
diff --git a/explorer-v2/src/components/staking/StakeTableWithAction.tsx b/explorer-v2/src/components/staking/StakeTableWithAction.tsx
index a31f10ed51..a32cbd78e0 100644
--- a/explorer-v2/src/components/staking/StakeTableWithAction.tsx
+++ b/explorer-v2/src/components/staking/StakeTableWithAction.tsx
@@ -43,8 +43,8 @@ const mappedNSApiNodes = (
name: cleanMoniker,
nodeId: node.node_id,
identity_key: node.identity_key,
- countryCode: node.geoip.country || null,
- countryName: countryName(node.geoip.country) || null,
+ countryCode: node.geoip?.country || null,
+ countryName: countryName(node.geoip?.country || null) || null,
profitMarginPercentage: node.rewarding_details
? +node.rewarding_details.cost_params.profit_margin_percent * 100
: 0,
diff --git a/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx
new file mode 100644
index 0000000000..49d2f720cd
--- /dev/null
+++ b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx
@@ -0,0 +1,58 @@
+"use client";
+import { Button, ButtonGroup } from "@mui/material";
+
+type Option = {
+ label: string;
+ isSelected: boolean;
+};
+
+type Options = [Option, Option, Option];
+
+const NodeFilterButtonGroup = ({
+ size = "small",
+ options,
+ onPage,
+ onFilterChange,
+}: {
+ size?: "small" | "medium" | "large";
+ options: Options;
+ onPage: string;
+ onFilterChange: (filter: "all" | "mixnodes" | "gateways") => void;
+}) => {
+ const handleClick = (label: string) => {
+ if (onPage === label) return;
+ switch (label) {
+ case "All nodes":
+ onFilterChange("all");
+ break;
+ case "Mixnodes":
+ onFilterChange("mixnodes");
+ break;
+ case "Gateways":
+ onFilterChange("gateways");
+ break;
+ }
+ };
+ return (
+
+ {options.map((option) => (
+
+ ))}
+
+ );
+};
+
+export default NodeFilterButtonGroup;