Yana/node page (#5276)
* Add nym-node page api WIP * nym-node page api WIP * Add rewards card * Add account balances * fix build * Add USD price to tokenomics card * fix build * fix build * fix build * Refactor ProgressBar * Add profile card * fix build * replace hardcoded id * Add build version and node roles * fix build * rename id param to address * add node table to explorer page * get node details from unstable endpoint + layout updates * allow node select from table * stop propogation on favorite/unfavorite * update self bond data * card refactors * revert node engine requirement * use node v20 --------- Co-authored-by: Yana <yanok87@users.noreply.github.com> Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
This commit is contained in:
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
- name: Build
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/cosmwasm-stargate": "^0.32.4",
|
||||
"@cosmjs/proto-signing": "^0.32.4",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
@@ -20,6 +22,7 @@
|
||||
"@mui/material-nextjs": "^6.1.9",
|
||||
"@mui/x-date-pickers": "^7.23.2",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@nymproject/contract-clients": "^1.4.1",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
@@ -36,6 +39,7 @@
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-data-grid": "7.1.1",
|
||||
"@mui/x-charts": "^7.22.3",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-world-flags": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { CurrencyRates, IAccountBalancesInfo } from "@/app/api/types";
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_ACCOUNT_ADDRESS, NYM_NODES, NYM_PRICES_API } from "@/app/api/urls";
|
||||
import { AccountBalancesCard } from "@/components/accountPageComponents/AccountBalancesCard";
|
||||
import { AccountInfoCard } from "@/components/accountPageComponents/AccountInfoCard";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Grid2, Typography } from "@mui/material";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ address: string }>;
|
||||
}) {
|
||||
try {
|
||||
const { address } = await params;
|
||||
|
||||
const accountData = await fetch(`${NYM_ACCOUNT_ADDRESS}${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const response = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymNodes: NodeData[] = await response.json();
|
||||
|
||||
const nymNode = nymNodes.find(
|
||||
(node) => node.bond_information.owner === address,
|
||||
);
|
||||
|
||||
const nymAccountBalancesData: IAccountBalancesInfo =
|
||||
await accountData.json();
|
||||
|
||||
if (!nymAccountBalancesData) {
|
||||
return <Typography>Account not found</Typography>;
|
||||
}
|
||||
|
||||
const nymPrice = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymPriceData: CurrencyRates = await nymPrice.json();
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={6}>
|
||||
<SectionHeading title="Account Details" />
|
||||
</Grid2>
|
||||
<Grid2 size={6} justifyContent="flex-end">
|
||||
<Box sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: false,
|
||||
link: nymNode
|
||||
? `/nym-node/${nymNode.node_id}`
|
||||
: "/nym-node/not-found",
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: true,
|
||||
link: "/account/1",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2 size={4}>
|
||||
<AccountInfoCard accountInfo={nymAccountBalancesData} />
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<AccountBalancesCard
|
||||
accountInfo={nymAccountBalancesData}
|
||||
nymPrice={nymPriceData.usd}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("error :>> ", error);
|
||||
return <Typography>Error loading account data</Typography>;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import ExplorerCard from "@/components/cards/ExplorerCard";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerListItem from "@/components/list/ListItem";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Grid2, Stack } from "@mui/material";
|
||||
|
||||
export default function Account() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={6}>
|
||||
<SectionHeading title="Account Details" />
|
||||
</Grid2>
|
||||
<Grid2 size={6} justifyContent="flex-end">
|
||||
<Box sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{ label: "Nym Node", isSelected: false, link: "/nym-node/1" },
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: true,
|
||||
link: "/account/1",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2 size={4}>
|
||||
<ExplorerCard label="Action" sx={{ height: "100%" }}>
|
||||
<div />
|
||||
</ExplorerCard>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<ExplorerCard label="Basic info">
|
||||
<Stack gap={1}>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="NYM Address"
|
||||
value="0x1234567890"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="Identity Key"
|
||||
value="0x1234567890"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Node bonded"
|
||||
value="24/11/2024"
|
||||
/>
|
||||
<ExplorerListItem row divider label="Nr. of stakes" value="56" />
|
||||
<ExplorerListItem row label="Self bonded" value="10,000 NYM" />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
NYM_NODES_DESCRIBED,
|
||||
NYM_NODE_DESCRIPTION,
|
||||
} from "./urls";
|
||||
|
||||
type Denom = "unym" | "nym";
|
||||
@@ -83,6 +84,96 @@ export interface ExplorerCache {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBondInfo {
|
||||
bond_information: {
|
||||
bonding_height: number;
|
||||
is_unbonding: boolean;
|
||||
node: {
|
||||
custom_http_port: number;
|
||||
host: string;
|
||||
identity_key: string;
|
||||
};
|
||||
node_id: number;
|
||||
original_pledge: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
owner: string;
|
||||
};
|
||||
rewarding_details: {
|
||||
cost_params: {
|
||||
profit_margin_percent: string;
|
||||
interval_operating_cost: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
};
|
||||
delegates: string;
|
||||
last_rewarded_epoch: number;
|
||||
operator: string;
|
||||
total_unit_reward: string;
|
||||
unique_delegations: number;
|
||||
unit_delegation: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface INodeDescription {
|
||||
contract_node_type: string;
|
||||
description: {
|
||||
authenticator: object;
|
||||
address: string;
|
||||
auxiliary_details: {
|
||||
location: string;
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
verloc_port: number | null;
|
||||
mix_port: number | 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;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
hostname: string | null;
|
||||
ip_address: 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 | null;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
wireguard: null | object;
|
||||
};
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(NYM_NODES_DESCRIBED, {
|
||||
@@ -114,21 +205,45 @@ const getExplorerData = async () => {
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [circulatingNymSupplyRes, nymNodesRes, packetsAndStakingRes] =
|
||||
await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
]);
|
||||
const fetchNymNodeDescription = await fetch(NYM_NODE_DESCRIPTION, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [circulatingNymSupplyData, nymNodesData, packetsAndStakingData] =
|
||||
await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
]);
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
nymNodeDescriptionRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchNymNodeDescription,
|
||||
]);
|
||||
|
||||
return [circulatingNymSupplyData, nymNodesData, packetsAndStakingData];
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
nymNodeDescriptionData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
nymNodeDescriptionRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
nymNodeDescriptionData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
|
||||
@@ -106,3 +106,44 @@ type NodeData = {
|
||||
};
|
||||
|
||||
export default NodeData;
|
||||
|
||||
export interface CurrencyRates {
|
||||
btc: number;
|
||||
chf: number;
|
||||
eur: number;
|
||||
timestamp: number;
|
||||
usd: number;
|
||||
}
|
||||
|
||||
// ACCOUNT BALANCES
|
||||
|
||||
export interface IRewardDetails {
|
||||
amount_staked: IAmountDetails;
|
||||
node_id: number;
|
||||
node_still_fully_bonded: boolean;
|
||||
rewards: IAmountDetails;
|
||||
}
|
||||
|
||||
export interface IAmountDetails {
|
||||
denom: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface IDelegationDetails {
|
||||
node_id: number;
|
||||
delegated: IAmountDetails;
|
||||
height: number;
|
||||
proxy: null | string;
|
||||
}
|
||||
|
||||
export interface IAccountBalancesInfo {
|
||||
accumulated_rewards: IRewardDetails[];
|
||||
address: string;
|
||||
balances: IAmountDetails[];
|
||||
claimable_rewards: IAmountDetails;
|
||||
delegations: IDelegationDetails[];
|
||||
operator_rewards?: null | IAmountDetails;
|
||||
total_delegations: IAmountDetails;
|
||||
total_value: IAmountDetails;
|
||||
vesting_account?: null | string;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,11 @@ export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const NYM_NODE_DESCRIPTION =
|
||||
"https://nym-api.swiss-staking.ch/v1/nym-nodes/described";
|
||||
export const NYM_NODE_BONDED =
|
||||
"https://nym-api.swiss-staking.ch/v1/nym-nodes/bonded";
|
||||
export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account/";
|
||||
export const NYM_PRICES_API =
|
||||
"https://canary-nym-vpn-chain-payment-watcher.nymte.ch/v1/price/average";
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import CardSkeleton from "@/components/cards/Skeleton";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Explorer page</Typography>
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: "Node",
|
||||
link: "/explorer",
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
link: "/stake",
|
||||
isSelected: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
<SectionHeading title="Explorer" />
|
||||
<Suspense fallback={<CardSkeleton sx={{ mt: 5 }} />}>
|
||||
<NodeTableWithAction />
|
||||
</Suspense>
|
||||
</Wrapper>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IAccountStatsCardProps } from "@/components/cards/AccountStatsCard";
|
||||
import type { IAccountBalancesTableProps } from "@/components/cards/AccountBalancesTable";
|
||||
import type { ContentCardProps } from "@/components/cards/MonoCard";
|
||||
|
||||
const explorerCard: ContentCardProps = {
|
||||
@@ -78,9 +78,9 @@ const explorerCard: ContentCardProps = {
|
||||
},
|
||||
};
|
||||
|
||||
const accountStatsCard: IAccountStatsCardProps = {
|
||||
overTitle: "Total value",
|
||||
priceTitle: 1990.0174,
|
||||
const accountStatsCard: IAccountBalancesTableProps = {
|
||||
// overTitle: "Total value",
|
||||
// priceTitle: 1990.0174,
|
||||
rows: [
|
||||
{ type: "Spendable", allocation: 15.53, amount: 12800, value: 1200 },
|
||||
{
|
||||
|
||||
@@ -1,139 +1,110 @@
|
||||
import ExplorerCard from "@/components/cards/ExplorerCard";
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_NODES } from "@/app/api/urls";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerListItem from "@/components/list/ListItem";
|
||||
import { StarRating } from "@/components/starRating";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
import { NodeMetricsCard } from "@/components/nymNodePageComponents/NodeMetricsCard";
|
||||
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
|
||||
import { NodeRewardsCard } from "@/components/nymNodePageComponents/NodeRewardsCard";
|
||||
import { QualityIndicatorsCard } from "@/components/nymNodePageComponents/QualityIndicatorsCard";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Grid2, Stack } from "@mui/material";
|
||||
import { Box, Grid2 } from "@mui/material";
|
||||
|
||||
export default async function NymNode({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string; account?: string }>;
|
||||
}) {
|
||||
const id = Number((await params).id);
|
||||
|
||||
const response = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymNodes: NodeData[] = await response.json();
|
||||
|
||||
if (!nymNodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nymNode = nymNodes.find((node) => node.node_id === id);
|
||||
|
||||
if (!nymNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function NymNode() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={6}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
</Grid2>
|
||||
<Grid2 size={6} justifyContent="flex-end">
|
||||
<Box sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<Grid2 size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{ label: "Nym Node", isSelected: true, link: "/nym-node/1" },
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: "/account/1",
|
||||
link: `/account/${nymNode.bond_information.owner}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2 size={4}>
|
||||
<ExplorerCard label="Action" sx={{ height: "100%" }}>
|
||||
<div />
|
||||
</ExplorerCard>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={4}>
|
||||
<ExplorerCard label="Basic info">
|
||||
<Stack gap={1}>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="NYM Address"
|
||||
value="0x1234567890"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="Identity Key"
|
||||
value="0x1234567890"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Node bonded"
|
||||
value="24/11/2024"
|
||||
/>
|
||||
<ExplorerListItem row divider label="Nr. of stakes" value="56" />
|
||||
<ExplorerListItem row label="Self bonded" value="10,000 NYM" />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
rewardDetails={nymNode.rewarding_details}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={4}>
|
||||
<ExplorerCard
|
||||
label="Node Rewards (Last Epoch/Hour)"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<ExplorerListItem row divider label="Role" value="Gateway" />
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Quality of service"
|
||||
value={<StarRating value={5} />}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Config score"
|
||||
value={<StarRating value={4} />}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Probe score"
|
||||
value={<StarRating value={5} />}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<QualityIndicatorsCard nodeDescription={nymNode.description} />
|
||||
</Grid2>
|
||||
<Grid2 size={6}>
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Total rew."
|
||||
value="10,000 NYM"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Operator rew."
|
||||
value="10,000 NYM"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Staker rew."
|
||||
value="10,000 NYM"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Profit margin rew."
|
||||
value="40 NYM"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Operating cost."
|
||||
value="40 NYM"
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeRewardsCard rewardDetails={nymNode.rewarding_details} />
|
||||
</Grid2>
|
||||
<Grid2 size={6}>
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem row divider label="Node ID." value="209" />
|
||||
<ExplorerListItem row divider label="Host" value="45.10.145.123" />
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Staker rew."
|
||||
value="10,000 NYM"
|
||||
/>
|
||||
<ExplorerListItem row divider label="Version" value="1.1.1.1" />
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Active set Prob."
|
||||
value="High"
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeMetricsCard
|
||||
nodeDescription={nymNode.description}
|
||||
nodeId={nymNode.bond_information.node_id}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</ContentLayout>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<SectionHeading title="Nym Node" />
|
||||
<Typography variant="body3">This Nym Node could not be found</Typography>
|
||||
</ContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
import type { IAccountBalancesInfo, IRewardDetails } from "@/app/api/types";
|
||||
import { AccountBalancesTable } from "../cards/AccountBalancesTable";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export interface IAccontStatsRowProps {
|
||||
type: string;
|
||||
allocation: number;
|
||||
amount: number;
|
||||
value: number;
|
||||
history?: { type: string; amount: number }[];
|
||||
isLastRow?: boolean;
|
||||
progressBarColor?: string;
|
||||
}
|
||||
|
||||
interface IAccountBalancesCardProps {
|
||||
accountInfo: IAccountBalancesInfo;
|
||||
nymPrice: number;
|
||||
}
|
||||
|
||||
const getNymsFormated = (unyms: number) => {
|
||||
const balance = unyms / 1000000;
|
||||
return balance;
|
||||
};
|
||||
const getPriceInUSD = (unyms: number, usdPrice: number) => {
|
||||
const balanceInUSD = (unyms / 1000000) * usdPrice;
|
||||
const balanceFormated = Number(balanceInUSD.toFixed(2));
|
||||
return balanceFormated;
|
||||
};
|
||||
|
||||
const getAllocation = (unyms: number, totalUnyms: number): number => {
|
||||
const allocationPercentage = (unyms * 100) / totalUnyms;
|
||||
return Number(allocationPercentage.toFixed(2));
|
||||
};
|
||||
|
||||
const calculateStakingRewards = (
|
||||
accumulatedRewards: IRewardDetails[],
|
||||
): number => {
|
||||
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
|
||||
return total + Number.parseFloat(rewardDetail.rewards.amount);
|
||||
}, 0);
|
||||
|
||||
const result = getNymsFormated(totalRewards);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
const { accountInfo, nymPrice } = props;
|
||||
|
||||
const totalBalanceUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_value.amount),
|
||||
nymPrice,
|
||||
);
|
||||
|
||||
const spendableNYM = getNymsFormated(Number(accountInfo.balances[0].amount));
|
||||
const spendableUSD = getPriceInUSD(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
nymPrice,
|
||||
);
|
||||
const spendableAllocation = getAllocation(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const delegationsNYM = getNymsFormated(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
);
|
||||
const delegationsUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
nymPrice,
|
||||
);
|
||||
const delegationsAllocation = getAllocation(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const claimableNYM = getNymsFormated(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
);
|
||||
const claimableUSD = getPriceInUSD(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
nymPrice,
|
||||
);
|
||||
const claimableAllocation = getAllocation(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const stakingRewards =
|
||||
accountInfo.accumulated_rewards.length > 0
|
||||
? calculateStakingRewards(accountInfo.accumulated_rewards)
|
||||
: 0;
|
||||
|
||||
const tableRows = [
|
||||
{
|
||||
type: "Spendable",
|
||||
allocation: spendableAllocation,
|
||||
amount: spendableNYM,
|
||||
value: spendableUSD,
|
||||
},
|
||||
{
|
||||
type: "Delegated",
|
||||
allocation: delegationsAllocation,
|
||||
amount: delegationsNYM,
|
||||
value: delegationsUSD,
|
||||
// history: [
|
||||
// { type: "Liquid", amount: 6900 },
|
||||
// { type: "Locked", amount: 6900 },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
type: "Claimable",
|
||||
allocation: claimableAllocation,
|
||||
amount: claimableNYM,
|
||||
value: claimableUSD,
|
||||
history: [
|
||||
// { type: "Unlocked", amount: 6900 },
|
||||
{
|
||||
type: "Staking rewards",
|
||||
amount: stakingRewards,
|
||||
},
|
||||
{ type: "Operator comission", amount: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Self bonded",
|
||||
allocation: 0,
|
||||
amount: 0,
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Total value"
|
||||
title={`$ ${totalBalanceUSD}`}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<AccountBalancesTable rows={tableRows} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import type { IAccountBalancesInfo } from "@/app/api/types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { CardQRCode } from "../qrCode/QrCode";
|
||||
|
||||
interface IAccountInfoCardProps {
|
||||
accountInfo: IAccountBalancesInfo;
|
||||
}
|
||||
|
||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
const { accountInfo } = props;
|
||||
|
||||
const balance = Number(accountInfo.balances[0].amount) / 1000000;
|
||||
const balanceFormated = `${balance} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Address"
|
||||
title={balanceFormated}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<Stack gap={5}>
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<CardQRCode url={accountInfo.address} />
|
||||
</Box>
|
||||
|
||||
<ExplorerListItem
|
||||
label="Address"
|
||||
value={
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">{accountInfo.address}</Typography>
|
||||
<CopyToClipboard text={accountInfo.address} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
+62
-82
@@ -4,7 +4,6 @@ import { TABLET_WIDTH } from "@/app/constants";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { Card, CardContent } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Table from "@mui/material/Table";
|
||||
@@ -148,7 +147,7 @@ const Row = (props: IAccontStatsRowProps) => {
|
||||
}}
|
||||
>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<CircleIcon sx={{ color: progressBarColor }} fontSize="small" />
|
||||
<CircleIcon sx={{ color: progressBarColor, fontSize: 12 }} />
|
||||
{type}
|
||||
</Box>
|
||||
</TableCell>
|
||||
@@ -235,14 +234,12 @@ const Row = (props: IAccontStatsRowProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccountStatsCardProps {
|
||||
export interface IAccountBalancesTableProps {
|
||||
rows: Array<IAccontStatsRowProps>;
|
||||
overTitle?: string;
|
||||
priceTitle?: number;
|
||||
}
|
||||
|
||||
export const AccountStatsCard = (props: IAccountStatsCardProps) => {
|
||||
const { rows, overTitle, priceTitle } = props;
|
||||
export const AccountBalancesTable = (props: IAccountBalancesTableProps) => {
|
||||
const { rows } = props;
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
const progressBarPercentages = () => {
|
||||
return rows.map((row) => row.allocation);
|
||||
@@ -262,80 +259,63 @@ export const AccountStatsCard = (props: IAccountStatsCardProps) => {
|
||||
const progressValues = getProgressValues();
|
||||
|
||||
return (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset", padding: 1 }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography
|
||||
mb={3}
|
||||
textTransform={"uppercase"}
|
||||
variant="h5"
|
||||
sx={{ color: "pine.600", letterSpacing: 0.7 }}
|
||||
>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{priceTitle && (
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }} mb={3}>
|
||||
${priceTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={row.type}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Box>
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={row.type}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
import CopyToClipboard from "@/components/copyToClipboard/CopyToClipboard";
|
||||
import { Box, Button, Card, CardContent, Typography } from "@mui/material";
|
||||
import Image from "next/image";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import type React from "react";
|
||||
import type { FC } from "react";
|
||||
import profileImagePlaceholder from "../../../public/profileImagePlaceholder.png";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
UpDownPriceIndicator,
|
||||
} from "../price/UpDownPriceIndicator";
|
||||
import type { IDynamicProgressBarProps } from "../progressBars/EpochProgressBar";
|
||||
import { CardQRCode, type ICardQRCodeProps } from "../qrCode/QrCode";
|
||||
import { StarRating } from "../starRating";
|
||||
|
||||
interface ICardTitlePriceProps {
|
||||
@@ -126,26 +126,6 @@ const CardCopyAddress = (props: ICardCopyAddressProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardQRCodeProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CardQRCode = (props: ICardQRCodeProps) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<Box
|
||||
padding={1}
|
||||
border={"1px solid #C3D7D7"}
|
||||
display={"block"}
|
||||
width={"unset"}
|
||||
>
|
||||
<QRCodeCanvas value={url} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardRatingsProps {
|
||||
ratings: Array<{ title: string; numberOfStars: number }>;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import { IconButton } from "@mui/material";
|
||||
|
||||
const Favorite = ({ onFavorite }: { onFavorite: () => void }) => {
|
||||
return (
|
||||
<IconButton onClick={onFavorite}>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFavorite();
|
||||
}}
|
||||
>
|
||||
<FavoriteBorderIcon sx={{ color: "accent.main" }} />
|
||||
</IconButton>
|
||||
);
|
||||
@@ -14,7 +19,12 @@ const Favorite = ({ onFavorite }: { onFavorite: () => void }) => {
|
||||
|
||||
const UnFavorite = ({ onUnfavorite }: { onUnfavorite: () => void }) => {
|
||||
return (
|
||||
<IconButton onClick={onUnfavorite}>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnfavorite();
|
||||
}}
|
||||
>
|
||||
<FavoriteIcon sx={{ color: "accent.main" }} />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
} from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IPacketsAndStakingData } from "@/app/api";
|
||||
import { HARBOURMASTER_API_MIXNODES_STATS } from "@/app/api/urls";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { LineChart } from "../lineChart";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import type { CurrencyRates } from "@/app/api/types";
|
||||
import { NYM_PRICES_API } from "@/app/api/urls";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { TitlePrice } from "../price/TitlePrice";
|
||||
|
||||
export const TokenomicsCard = async () => {
|
||||
const titlePrice = {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
const nymPrice = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymPriceData: CurrencyRates = await nymPrice.json();
|
||||
const nymPriceDataFormated = Number(nymPriceData.usd.toFixed(2));
|
||||
|
||||
const titlePrice = {
|
||||
price: nymPriceDataFormated,
|
||||
// upDownLine: {
|
||||
// percentage: 10,
|
||||
// numberWentUp: true,
|
||||
// },
|
||||
};
|
||||
const dataRows = [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite, UnFavorite } from "../favorite/Favorite";
|
||||
@@ -25,6 +26,8 @@ const ColumnHeading = ({
|
||||
};
|
||||
|
||||
const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [favorites, saveFavorites] = useLocalStorage<string[]>(
|
||||
"nym-node-favorites",
|
||||
[],
|
||||
@@ -195,8 +198,11 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: {
|
||||
hover: false,
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
onClick: () => {
|
||||
router.push(`/nym-node/${row.original.nodeId}`);
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
":nth-child(odd)": {
|
||||
bgcolor: "#F3F7FB !important",
|
||||
@@ -204,8 +210,9 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
":nth-child(even)": {
|
||||
bgcolor: "white !important",
|
||||
},
|
||||
cursor: "pointer",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
return <MaterialReactTable table={table} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type {
|
||||
BondInformation,
|
||||
NodeDescription,
|
||||
RewardingDetails,
|
||||
} from "@/app/api/types";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { format } from "date-fns";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface IBasicInfoCardProps {
|
||||
bondInfo: BondInformation;
|
||||
nodeDescription: NodeDescription;
|
||||
rewardDetails: RewardingDetails;
|
||||
}
|
||||
|
||||
export const BasicInfoCard = (props: IBasicInfoCardProps) => {
|
||||
const { bondInfo, nodeDescription, rewardDetails } = props;
|
||||
|
||||
const timeBonded = format(
|
||||
new Date(nodeDescription.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
|
||||
const selfBond = formatBigNum(Number(rewardDetails.operator) / 1_000_000);
|
||||
const selfBondFormated = `${selfBond} NYM`;
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Stack gap={1}>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="NYM Address"
|
||||
value={
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">{bondInfo.owner}</Typography>
|
||||
<CopyToClipboard text={bondInfo.owner} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
label="Identity Key"
|
||||
value={
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography variant="body4">
|
||||
{bondInfo.node.identity_key}
|
||||
</Typography>
|
||||
<CopyToClipboard text={bondInfo.node.identity_key} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem row divider label="Node bonded" value={timeBonded} />
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakes"
|
||||
value={rewardDetails.unique_delegations.toString()}
|
||||
/>
|
||||
<ExplorerListItem row label="Self bonded" value={selfBondFormated} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { NodeDescription } from "@/app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface INodeMetricsCardProps {
|
||||
nodeDescription: NodeDescription;
|
||||
nodeId: number;
|
||||
}
|
||||
|
||||
export const NodeMetricsCard = (props: INodeMetricsCardProps) => {
|
||||
const { nodeDescription, nodeId } = props;
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Node ID."
|
||||
value={nodeId.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeDescription.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeDescription.build_information.build_version}
|
||||
/>
|
||||
<ExplorerListItem row label="Active set Prob." value="High" />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import type { BondInformation, NodeDescription } from "@/app/api/types";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { RandomAvatar } from "react-random-avatars";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
|
||||
interface INodeProfileCardProps {
|
||||
bondInfo: BondInformation;
|
||||
nodeDescription: NodeDescription;
|
||||
}
|
||||
|
||||
export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
const { bondInfo, nodeDescription } = props;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Stack gap={1}>
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<RandomAvatar name={bondInfo.node.identity_key} size={80} square />
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h3"
|
||||
mt={3}
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{"Moniker"}
|
||||
</Typography>
|
||||
<CountryFlag
|
||||
countryCode={nodeDescription.auxiliary_details.location || ""}
|
||||
/>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Team of professional validators with best digital solutions. Please
|
||||
visit our Telegram🔹https://t.me/CryptoSailorsAnn🔹
|
||||
</Typography>
|
||||
<Box mt={3}>
|
||||
<Button variant="contained" size="small">
|
||||
Stake on node
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { RewardingDetails } from "@/app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
interface INodeRewardsCardProps {
|
||||
rewardDetails: RewardingDetails;
|
||||
}
|
||||
|
||||
export const NodeRewardsCard = (props: INodeRewardsCardProps) => {
|
||||
const { rewardDetails } = props;
|
||||
|
||||
const totalRewards = Number(rewardDetails.total_unit_reward) / 1000000;
|
||||
const totalRewardsFormated = `${totalRewards} NYM`;
|
||||
|
||||
const operatorRewards = Number(rewardDetails.operator) / 1000000;
|
||||
const operatorRewardsFormated = `${operatorRewards} NYM`;
|
||||
|
||||
const stakerRewards = Number(rewardDetails.delegates) / 1000000;
|
||||
const stakerRewardsFormated = `${stakerRewards} 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;
|
||||
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Node rewards(last epoch/hour)" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Total rew."
|
||||
value={totalRewardsFormated}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Operator rew."
|
||||
value={operatorRewardsFormated}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Staker rew."
|
||||
value={stakerRewardsFormated}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Profit margin rew."
|
||||
value={profitMarginPercentFormated}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
label="Operating cost."
|
||||
value={operatingCostsFormated}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { NodeDescription } from "@/app/api/types";
|
||||
import { Chip, Stack } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import StarRating from "../starRating/StarRating";
|
||||
|
||||
interface IQualityIndicatorsCardProps {
|
||||
nodeDescription: NodeDescription;
|
||||
}
|
||||
|
||||
type DelcaredRoleKey = keyof NodeDescription["declared_role"];
|
||||
type RoleString = "Entry Node" | "Exit IPR Node" | "Exit NR Node" | "Mix Node";
|
||||
|
||||
const roleMapping: Record<DelcaredRoleKey, RoleString> = {
|
||||
entry: "Entry Node",
|
||||
exit_ipr: "Exit IPR Node",
|
||||
exit_nr: "Exit NR Node",
|
||||
mixnode: "Mix Node",
|
||||
};
|
||||
|
||||
function getNodeRoles(
|
||||
declaredRoles: NodeDescription["declared_role"],
|
||||
): RoleString[] {
|
||||
const activeRoles = Object.entries(declaredRoles)
|
||||
.filter(([, isActive]) => isActive)
|
||||
.map(([role]) => roleMapping[role as DelcaredRoleKey]);
|
||||
|
||||
return activeRoles;
|
||||
}
|
||||
|
||||
export const QualityIndicatorsCard = (props: IQualityIndicatorsCardProps) => {
|
||||
const { nodeDescription } = props;
|
||||
|
||||
const nodeRoles = getNodeRoles(nodeDescription.declared_role);
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
<Stack key={role} direction="row" gap={1}>
|
||||
<Chip key={role} label={role} size="small" />
|
||||
</Stack>
|
||||
));
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Quality indicatiors" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Role"
|
||||
value={
|
||||
<Stack direction="row" gap={1}>
|
||||
{NodeRoles}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Quality of service"
|
||||
value={<StarRating value={5} />}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Config score"
|
||||
value={<StarRating value={4} />}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Probe score"
|
||||
value={<StarRating value={5} />}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
|
||||
interface ITitlePriceProps {
|
||||
price: number;
|
||||
upDownLine: IUpDownPriceIndicatorProps;
|
||||
upDownLine?: IUpDownPriceIndicatorProps;
|
||||
}
|
||||
export const TitlePrice = (props: ITitlePriceProps): React.ReactNode => {
|
||||
const { price, upDownLine } = props;
|
||||
@@ -25,7 +25,7 @@ export const TitlePrice = (props: ITitlePriceProps): React.ReactNode => {
|
||||
${price}
|
||||
</Typography>
|
||||
</Box>
|
||||
<UpDownPriceIndicator {...upDownLine} />
|
||||
{upDownLine && <UpDownPriceIndicator {...upDownLine} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,60 @@
|
||||
"use client";
|
||||
import { Stack } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import { addHours, differenceInMinutes, format } from "date-fns";
|
||||
import { addHours, format } from "date-fns";
|
||||
import React from "react";
|
||||
import ListItem from "../list/ListItem";
|
||||
import ProgressBar from "../progressBar/ProgressBar";
|
||||
import ProgressBar from "./ProgressBar";
|
||||
|
||||
export interface IDynamicProgressBarProps {
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
const EpochProgressBar = async ({
|
||||
start,
|
||||
showEpoch,
|
||||
}: IDynamicProgressBarProps) => {
|
||||
const EpochProgressBar = ({ start, showEpoch }: IDynamicProgressBarProps) => {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
const startDate = new Date(start);
|
||||
const endDate = addHours(new Date(start), 1);
|
||||
const startTime = format(startDate, "HH:mm dd-MM-yyyy");
|
||||
const endTime = format(endDate, "HH:mm dd-MM-yyyy");
|
||||
const totalEpochTime = differenceInMinutes(endDate, startDate);
|
||||
|
||||
const progress =
|
||||
(differenceInMinutes(new Date(), startDate) / totalEpochTime) * 100;
|
||||
React.useEffect(() => {
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate start timestamp
|
||||
if (Number.isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to calculate progress
|
||||
const calculateProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime < startTime) {
|
||||
return 0;
|
||||
}
|
||||
if (currentTime >= endTime) {
|
||||
return 100;
|
||||
}
|
||||
const elapsed = currentTime - startTime;
|
||||
const total = endTime - startTime;
|
||||
return (elapsed / total) * 100;
|
||||
};
|
||||
|
||||
// Set initial progress and start timer
|
||||
setProgress(calculateProgress());
|
||||
const timer = setInterval(() => {
|
||||
setProgress(calculateProgress());
|
||||
}, 60000); // Update every minute (60000 milliseconds)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
|
||||
export interface ICardQRCodeProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const CardQRCode = (props: ICardQRCodeProps) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box
|
||||
border={"1px solid #C3D7D7"}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
width={144}
|
||||
height={144}
|
||||
>
|
||||
<QRCodeCanvas value={url} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,6 @@
|
||||
"sdk/typescript/packages/validator-client",
|
||||
"ts-packages/*",
|
||||
"nym-wallet",
|
||||
"explorer",
|
||||
"explorer-nextjs",
|
||||
"types",
|
||||
"clients/validator"
|
||||
|
||||
-6508
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user