Skip to content

feat: add more info to overview display #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/OverviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ export const OverviewCard = ({
<Heading as="h3" size="sm">
{title}
</Heading>
<Flex w="100%" h="6" alignItems="center" fontSize="sm">
<Flex
w="100%"
minH="6"
alignItems="center"
fontSize="sm"
wordBreak="break-all"
whiteSpace="normal"
>
{isLoading ? (
<SkeletonText
variant="shine"
Expand Down
69 changes: 66 additions & 3 deletions src/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,53 @@ import { OverviewCard } from "../components/OverviewCard";
import {
Box,
Flex,
Grid,
Heading,
HStack,
SkeletonText,
VStack,
} from "@chakra-ui/react";
import { useLatestEpoch } from "../queries/useLatestEpoch";
import { useChainParameters } from "../queries/useChainParameters";
import { useAccount } from "../queries/useAccount";
import { useTokenSupply } from "../queries/useTokenSupply";
import { useVotingPower } from "../queries/useVotingPower";
import { useBlockInfo } from "../queries/useBlockInfo";
import { BlockList } from "../components/BlockList";
import { FaListAlt } from "react-icons/fa";
import { FaCubes } from "react-icons/fa6";
import { NAMADA_ADDRESS, PGF_ADDRESS, toDisplayAmount, toDisplayAmountFancy, formatNumberWithCommas } from "../utils";
import namadaAssets from "@namada/chain-registry/namada/assetlist.json";
import type { Asset } from "@chain-registry/types";
import BigNumber from "bignumber.js";
import { useMemo } from "react";

export const Index = () => {
const latestBlock = useLatestBlock();
const latestEpoch = useLatestEpoch();
const chainParameters = useChainParameters();
const pgfBalance = useAccount(PGF_ADDRESS);
const namSupply = useTokenSupply(NAMADA_ADDRESS);
const votingPower = useVotingPower();

const pgfBalanceNam = pgfBalance.data?.find((b: any) => b.tokenAddress === NAMADA_ADDRESS)?.minDenomAmount ?? null;

// Calculate staked NAM percentage
const stakedNam = votingPower.data?.totalVotingPower ?? null;
const denomSupply = useMemo(() => {
if (!namSupply.data?.effectiveSupply) return 0;
return toDisplayAmount(namadaAssets.assets[0] as Asset, new BigNumber(namSupply.data.effectiveSupply)).toNumber();
}, [namSupply.data?.effectiveSupply]);
const stakedNamPercentage = useMemo(() => {
return stakedNam && denomSupply ? (stakedNam / denomSupply * 100).toFixed(2) : null;
}, [stakedNam, denomSupply]);

// Calculate average block time
const windowSize = 5;
const latestBlockInfo = useBlockInfo(latestBlock.data?.block - 1);
const previousBlockInfo = useBlockInfo(latestBlock.data?.block ? latestBlock.data?.block - 1 - windowSize : null);
const avgBlockTime = latestBlockInfo?.data?.timestamp && previousBlockInfo?.data?.timestamp ?
(latestBlockInfo.data.timestamp - previousBlockInfo.data.timestamp) / windowSize : null;

return (
<VStack gap={8} align="start">
<Box>
Expand All @@ -25,15 +59,44 @@ export const Index = () => {
Overview
</Flex>
</Heading>
<HStack columns={3}>

<Grid
templateColumns={{
base: "repeat(1, 1fr)",
sm: "repeat(2, 1fr)",
md: "repeat(3, 1fr)",
lg: "repeat(4, 1fr)"
}}
gap={2}
w="100%"
>
<OverviewCard title="Chain ID" isLoading={chainParameters.isLoading}>
{chainParameters.data?.chainId}
</OverviewCard>
<OverviewCard title="Latest Block" isLoading={latestBlock.isLoading}>
{latestBlock.data?.block}
</OverviewCard>
<OverviewCard title="Latest Epoch" isLoading={latestEpoch.isLoading}>
{latestEpoch.data?.epoch}
</OverviewCard>
</HStack>
<OverviewCard title="Block Time" isLoading={previousBlockInfo?.isLoading}>
{avgBlockTime ? `${avgBlockTime.toFixed(1)}s` : ""}
</OverviewCard>
<OverviewCard title="Effective Supply (NAM)" isLoading={namSupply.isLoading}>
{toDisplayAmountFancy(namadaAssets.assets[0] as Asset, new BigNumber(namSupply.data?.effectiveSupply))}
</OverviewCard>
<OverviewCard title="Staked (NAM)" isLoading={votingPower.isLoading || namSupply.isLoading}>
{formatNumberWithCommas(votingPower.data?.totalVotingPower ?? 0)} ({stakedNamPercentage}%)
</OverviewCard>
<OverviewCard title="Staking APR" isLoading={chainParameters.isLoading}>
{(chainParameters.data?.apr * 100).toFixed(2)}%
</OverviewCard>
<OverviewCard title="PGF Balance" isLoading={pgfBalance.isLoading}>
{toDisplayAmountFancy(namadaAssets.assets[0] as Asset, new BigNumber(pgfBalanceNam))}
</OverviewCard>
</Grid>
</Box>

<Box w="100%">
<Heading as="h1" size="xl" mb={3} color="cyan">
<Flex gap={2} align="center">
Expand Down
9 changes: 8 additions & 1 deletion src/queries/useBlockInfo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { useQuery } from "@tanstack/react-query";
import { get } from "../http/query";

export const useBlockInfo = (blockHeight: number) => {
export const useBlockInfo = (blockHeight: number | null | undefined) => {
// Validate that blockHeight is a positive number
const isValidBlockHeight = blockHeight != null && blockHeight > 0 && Number.isInteger(blockHeight);

return useQuery({
queryKey: ["block", blockHeight],
queryFn: async () => {
if (!isValidBlockHeight) {
throw new Error(`Invalid block height: ${blockHeight}`);
}
return get("/block/height/" + blockHeight);
},
enabled: isValidBlockHeight,
staleTime: Infinity,
gcTime: Infinity,
});
Expand Down
4 changes: 2 additions & 2 deletions src/queries/useTokenSupply.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSimpleGet } from "./useSimpleGet";

export const useTokenSupply = () => {
return useSimpleGet("tokenSupply", "/chain/token-supply");
export const useTokenSupply = (address: string) => {
return useSimpleGet("tokenSupply", `/chain/token-supply?address=${address}`);
};
5 changes: 5 additions & 0 deletions src/queries/useVotingPower.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useSimpleGet } from "./useSimpleGet";

export const useVotingPower = () => {
return useSimpleGet("tokenSupply", "/pos/voting-power");
};
42 changes: 42 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const camelCaseToTitleCase = (str: string) => {
};

export const NAMADA_ADDRESS = "tnam1q9gr66cvu4hrzm0sd5kmlnjje82gs3xlfg3v6nu7";
export const PGF_ADDRESS = "tnam1pgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkhgajr";

export const shortenHashOrAddress = (hash: string | null, length = 10) => {
if (!hash) return "-";
Expand All @@ -35,3 +36,44 @@ export const formatTimestamp = (timestamp: number): string => {
const time = fromUnixTime(timestamp);
return format(time, "PPpp");
};

export const formatNumberWithCommas = (num: number | BigNumber): string => {
const numericValue = typeof num === 'number' ? num :
(num && typeof num.toNumber === 'function') ? num.toNumber() : Number(num);
return numericValue.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
};

const getMagnitudeSuffix = (num: number): string => {
if (num >= 1_000_000_000) {
return `${(num / 1_000_000_000).toFixed(1)} B`;
} else if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)} M`;
} else if (num >= 1_000) {
return `${(num / 1_000).toFixed(1)} K`;
}
return "";
};

export const toDisplayAmountFancy = (
asset: Asset,
baseAmount: BigNumber,
): string => {
const displayAmount = toDisplayAmount(asset, baseAmount);
const numericValue = displayAmount.toNumber();

const formattedWithCommas = numericValue.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});

const magnitudeSuffix = getMagnitudeSuffix(numericValue);

if (magnitudeSuffix) {
return `${formattedWithCommas} (${magnitudeSuffix})`;
}

return formattedWithCommas;
};