2020-08-01 07:05:58 -07:00
|
|
|
import React from "react";
|
2020-08-07 23:45:57 -07:00
|
|
|
import { TableCardBody } from "components/common/TableCardBody";
|
2020-09-18 05:41:53 -07:00
|
|
|
import { Slot } from "components/common/Slot";
|
2020-08-01 07:05:58 -07:00
|
|
|
import {
|
2020-10-19 20:11:48 -07:00
|
|
|
ClusterStatsStatus,
|
2020-08-01 07:05:58 -07:00
|
|
|
useDashboardInfo,
|
|
|
|
usePerformanceInfo,
|
2020-10-19 20:11:48 -07:00
|
|
|
useStatsProvider,
|
|
|
|
} from "providers/stats/solanaClusterStats";
|
2021-07-20 09:43:17 -07:00
|
|
|
import { abbreviatedNumber, lamportsToSol, slotsToHumanString } from "utils";
|
2021-03-25 09:59:50 -07:00
|
|
|
import { ClusterStatus, useCluster } from "providers/cluster";
|
2022-04-29 22:37:13 -07:00
|
|
|
import { LiveTransactionStatsCard } from "components/LiveTransactionStatsCard";
|
2022-01-21 20:41:41 -08:00
|
|
|
import { displayTimestampWithoutDate } from "utils/date";
|
2021-03-25 09:59:50 -07:00
|
|
|
import { Status, useFetchSupply, useSupply } from "providers/supply";
|
|
|
|
import { ErrorCard } from "components/common/ErrorCard";
|
|
|
|
import { LoadingCard } from "components/common/LoadingCard";
|
|
|
|
import { useVoteAccounts } from "providers/accounts/vote-accounts";
|
2021-05-17 12:43:18 -07:00
|
|
|
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
2021-09-29 20:08:19 -07:00
|
|
|
import { Epoch } from "components/common/Epoch";
|
2022-01-21 20:41:41 -08:00
|
|
|
import { TimestampToggle } from "components/common/TimestampToggle";
|
2021-03-25 09:59:50 -07:00
|
|
|
|
|
|
|
const CLUSTER_STATS_TIMEOUT = 5000;
|
2020-10-19 20:11:48 -07:00
|
|
|
|
2020-08-07 23:45:57 -07:00
|
|
|
export function ClusterStatsPage() {
|
2020-08-01 07:05:58 -07:00
|
|
|
return (
|
2020-08-07 23:45:57 -07:00
|
|
|
<div className="container mt-4">
|
2021-03-25 09:59:50 -07:00
|
|
|
<StakingComponent />
|
2020-08-07 23:45:57 -07:00
|
|
|
<div className="card">
|
|
|
|
<div className="card-header">
|
|
|
|
<div className="row align-items-center">
|
|
|
|
<div className="col">
|
|
|
|
<h4 className="card-header-title">Live Cluster Stats</h4>
|
|
|
|
</div>
|
2020-08-01 07:05:58 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
2020-08-07 23:45:57 -07:00
|
|
|
<StatsCardBody />
|
2020-08-01 07:05:58 -07:00
|
|
|
</div>
|
2022-04-29 22:37:13 -07:00
|
|
|
<LiveTransactionStatsCard />
|
2020-08-01 07:05:58 -07:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-25 09:59:50 -07:00
|
|
|
function StakingComponent() {
|
|
|
|
const { status } = useCluster();
|
|
|
|
const supply = useSupply();
|
|
|
|
const fetchSupply = useFetchSupply();
|
|
|
|
const coinInfo = useCoinGecko("solana");
|
|
|
|
const { fetchVoteAccounts, voteAccounts } = useVoteAccounts();
|
|
|
|
|
|
|
|
function fetchData() {
|
|
|
|
fetchSupply();
|
|
|
|
fetchVoteAccounts();
|
|
|
|
}
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (status === ClusterStatus.Connected) {
|
|
|
|
fetchData();
|
|
|
|
}
|
|
|
|
}, [status]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
2021-04-10 12:23:56 -07:00
|
|
|
const delinquentStake = React.useMemo(() => {
|
2021-03-25 09:59:50 -07:00
|
|
|
if (voteAccounts) {
|
|
|
|
return voteAccounts.delinquent.reduce(
|
|
|
|
(prev, current) => prev + current.activatedStake,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}, [voteAccounts]);
|
|
|
|
|
2021-04-10 12:23:56 -07:00
|
|
|
const activeStake = React.useMemo(() => {
|
|
|
|
if (voteAccounts && delinquentStake) {
|
|
|
|
return (
|
|
|
|
voteAccounts.current.reduce(
|
|
|
|
(prev, current) => prev + current.activatedStake,
|
|
|
|
0
|
|
|
|
) + delinquentStake
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}, [voteAccounts, delinquentStake]);
|
2021-03-25 09:59:50 -07:00
|
|
|
|
|
|
|
if (supply === Status.Disconnected) {
|
|
|
|
// we'll return here to prevent flicker
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-04-10 12:23:56 -07:00
|
|
|
if (supply === Status.Idle || supply === Status.Connecting || !coinInfo) {
|
2021-07-20 09:43:17 -07:00
|
|
|
return <LoadingCard message="Loading supply and price data" />;
|
2021-03-25 09:59:50 -07:00
|
|
|
} else if (typeof supply === "string") {
|
|
|
|
return <ErrorCard text={supply} retry={fetchData} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
const circulatingPercentage = (
|
|
|
|
(supply.circulating / supply.total) *
|
|
|
|
100
|
|
|
|
).toFixed(1);
|
|
|
|
|
|
|
|
let delinquentStakePercentage;
|
2021-04-10 12:23:56 -07:00
|
|
|
if (delinquentStake && activeStake) {
|
|
|
|
delinquentStakePercentage = ((delinquentStake / activeStake) * 100).toFixed(
|
|
|
|
1
|
|
|
|
);
|
2021-03-25 09:59:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
let solanaInfo;
|
|
|
|
if (coinInfo.status === CoingeckoStatus.Success) {
|
|
|
|
solanaInfo = coinInfo.coinInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2021-07-20 09:43:17 -07:00
|
|
|
<div className="row staking-card">
|
|
|
|
<div className="col-12 col-lg-4 col-xl">
|
|
|
|
<div className="card">
|
|
|
|
<div className="card-body">
|
2021-03-25 09:59:50 -07:00
|
|
|
<h4>Circulating Supply</h4>
|
|
|
|
<h1>
|
|
|
|
<em>{displayLamports(supply.circulating)}</em> /{" "}
|
|
|
|
<small>{displayLamports(supply.total)}</small>
|
|
|
|
</h1>
|
|
|
|
<h5>
|
|
|
|
<em>{circulatingPercentage}%</em> is circulating
|
|
|
|
</h5>
|
|
|
|
</div>
|
2021-07-20 09:43:17 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="col-12 col-lg-4 col-xl">
|
|
|
|
<div className="card">
|
|
|
|
<div className="card-body">
|
2021-03-25 09:59:50 -07:00
|
|
|
<h4>Active Stake</h4>
|
2021-04-10 12:23:56 -07:00
|
|
|
{activeStake && (
|
|
|
|
<h1>
|
|
|
|
<em>{displayLamports(activeStake)}</em> /{" "}
|
|
|
|
<small>{displayLamports(supply.total)}</small>
|
|
|
|
</h1>
|
|
|
|
)}
|
2021-03-25 09:59:50 -07:00
|
|
|
{delinquentStakePercentage && (
|
|
|
|
<h5>
|
|
|
|
Delinquent stake: <em>{delinquentStakePercentage}%</em>
|
|
|
|
</h5>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-07-20 09:43:17 -07:00
|
|
|
</div>
|
|
|
|
<div className="col-12 col-lg-4 col-xl">
|
|
|
|
<div className="card">
|
|
|
|
<div className="card-body">
|
|
|
|
{solanaInfo && (
|
|
|
|
<>
|
|
|
|
<h4>
|
|
|
|
Price{" "}
|
2021-11-28 12:49:22 -08:00
|
|
|
<span className="ms-2 badge bg-primary rank">
|
2021-07-20 09:43:17 -07:00
|
|
|
Rank #{solanaInfo.market_cap_rank}
|
|
|
|
</span>
|
|
|
|
</h4>
|
|
|
|
<h1>
|
|
|
|
<em>${solanaInfo.price.toFixed(2)}</em>{" "}
|
|
|
|
{solanaInfo.price_change_percentage_24h > 0 && (
|
|
|
|
<small className="change-positive">
|
|
|
|
↑ {solanaInfo.price_change_percentage_24h.toFixed(2)}
|
|
|
|
%
|
|
|
|
</small>
|
|
|
|
)}
|
|
|
|
{solanaInfo.price_change_percentage_24h < 0 && (
|
|
|
|
<small className="change-negative">
|
|
|
|
↓ {solanaInfo.price_change_percentage_24h.toFixed(2)}
|
|
|
|
%
|
|
|
|
</small>
|
|
|
|
)}
|
|
|
|
{solanaInfo.price_change_percentage_24h === 0 && (
|
|
|
|
<small>0%</small>
|
|
|
|
)}
|
|
|
|
</h1>
|
|
|
|
<h5>
|
|
|
|
24h Vol: <em>${abbreviatedNumber(solanaInfo.volume_24)}</em>{" "}
|
|
|
|
MCap: <em>${abbreviatedNumber(solanaInfo.market_cap)}</em>
|
|
|
|
</h5>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{coinInfo.status === CoingeckoStatus.FetchFailed && (
|
|
|
|
<>
|
|
|
|
<h4>Price</h4>
|
|
|
|
<h1>
|
|
|
|
<em>$--.--</em>
|
|
|
|
</h1>
|
|
|
|
<h5>Error fetching the latest price information</h5>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{solanaInfo && (
|
|
|
|
<p className="updated-time text-muted">
|
|
|
|
Updated at{" "}
|
|
|
|
{displayTimestampWithoutDate(solanaInfo.last_updated.getTime())}
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-03-25 09:59:50 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function displayLamports(value: number) {
|
|
|
|
return abbreviatedNumber(lamportsToSol(value));
|
|
|
|
}
|
|
|
|
|
2020-08-01 07:05:58 -07:00
|
|
|
function StatsCardBody() {
|
|
|
|
const dashboardInfo = useDashboardInfo();
|
|
|
|
const performanceInfo = usePerformanceInfo();
|
2020-10-19 20:11:48 -07:00
|
|
|
const { setActive } = useStatsProvider();
|
2020-08-01 07:05:58 -07:00
|
|
|
const { cluster } = useCluster();
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2020-10-19 20:11:48 -07:00
|
|
|
setActive(true);
|
|
|
|
return () => setActive(false);
|
|
|
|
}, [setActive, cluster]);
|
2020-08-01 07:05:58 -07:00
|
|
|
|
2020-10-19 20:11:48 -07:00
|
|
|
if (
|
|
|
|
performanceInfo.status !== ClusterStatsStatus.Ready ||
|
|
|
|
dashboardInfo.status !== ClusterStatsStatus.Ready
|
|
|
|
) {
|
|
|
|
const error =
|
|
|
|
performanceInfo.status === ClusterStatsStatus.Error ||
|
|
|
|
dashboardInfo.status === ClusterStatsStatus.Error;
|
|
|
|
return <StatsNotReady error={error} />;
|
2020-08-01 07:05:58 -07:00
|
|
|
}
|
|
|
|
|
2021-05-24 21:24:11 -07:00
|
|
|
const { avgSlotTime_1h, avgSlotTime_1min, epochInfo, blockTime } =
|
|
|
|
dashboardInfo;
|
2020-10-19 20:11:48 -07:00
|
|
|
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
2020-10-28 22:17:45 -07:00
|
|
|
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
|
2020-08-01 07:05:58 -07:00
|
|
|
const { slotIndex, slotsInEpoch } = epochInfo;
|
|
|
|
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
2020-08-04 07:18:09 -07:00
|
|
|
const epochTimeRemaining = slotsToHumanString(
|
|
|
|
slotsInEpoch - slotIndex,
|
2020-10-19 20:11:48 -07:00
|
|
|
hourlySlotTime
|
2020-08-04 07:18:09 -07:00
|
|
|
);
|
2020-09-18 05:41:53 -07:00
|
|
|
const { blockHeight, absoluteSlot } = epochInfo;
|
2020-08-01 07:05:58 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<TableCardBody>
|
|
|
|
<tr>
|
2020-08-07 03:15:23 -07:00
|
|
|
<td className="w-100">Slot</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">
|
2020-10-28 19:45:44 -07:00
|
|
|
<Slot slot={absoluteSlot} link />
|
2020-09-18 05:41:53 -07:00
|
|
|
</td>
|
2020-08-07 03:15:23 -07:00
|
|
|
</tr>
|
2020-10-19 20:11:48 -07:00
|
|
|
{blockHeight !== undefined && (
|
|
|
|
<tr>
|
|
|
|
<td className="w-100">Block height</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">
|
2020-10-19 20:11:48 -07:00
|
|
|
<Slot slot={blockHeight} />
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)}
|
2021-01-05 14:22:12 -08:00
|
|
|
{blockTime && (
|
|
|
|
<tr>
|
2021-01-05 21:00:36 -08:00
|
|
|
<td className="w-100">Cluster time</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">
|
2022-01-21 20:41:41 -08:00
|
|
|
<TimestampToggle unixTimestamp={blockTime}></TimestampToggle>
|
2021-01-05 14:22:12 -08:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)}
|
2020-08-07 03:15:23 -07:00
|
|
|
<tr>
|
2020-10-28 22:17:45 -07:00
|
|
|
<td className="w-100">Slot time (1min average)</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">{averageSlotTime}ms</td>
|
2020-10-28 22:17:45 -07:00
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td className="w-100">Slot time (1hr average)</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">{hourlySlotTime}ms</td>
|
2020-08-01 07:05:58 -07:00
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td className="w-100">Epoch</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">
|
2021-09-29 20:08:19 -07:00
|
|
|
<Epoch epoch={epochInfo.epoch} link />
|
|
|
|
</td>
|
2020-08-01 07:05:58 -07:00
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td className="w-100">Epoch progress</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">{epochProgress}</td>
|
2020-08-01 07:05:58 -07:00
|
|
|
</tr>
|
|
|
|
<tr>
|
2020-10-28 22:17:45 -07:00
|
|
|
<td className="w-100">Epoch time remaining (approx.)</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-lg-end font-monospace">~{epochTimeRemaining}</td>
|
2020-08-01 07:05:58 -07:00
|
|
|
</tr>
|
|
|
|
</TableCardBody>
|
|
|
|
);
|
|
|
|
}
|
2020-10-19 20:11:48 -07:00
|
|
|
|
|
|
|
export function StatsNotReady({ error }: { error: boolean }) {
|
|
|
|
const { setTimedOut, retry, active } = useStatsProvider();
|
|
|
|
const { cluster } = useCluster();
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
let timedOut = 0;
|
|
|
|
if (!error) {
|
|
|
|
timedOut = setTimeout(setTimedOut, CLUSTER_STATS_TIMEOUT);
|
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
if (timedOut) {
|
|
|
|
clearTimeout(timedOut);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [setTimedOut, cluster, error]);
|
|
|
|
|
|
|
|
if (error || !active) {
|
|
|
|
return (
|
|
|
|
<div className="card-body text-center">
|
|
|
|
There was a problem loading cluster stats.{" "}
|
|
|
|
<button
|
|
|
|
className="btn btn-white btn-sm"
|
|
|
|
onClick={() => {
|
|
|
|
retry();
|
|
|
|
}}
|
|
|
|
>
|
2021-11-28 12:49:22 -08:00
|
|
|
<span className="fe fe-refresh-cw me-2"></span>
|
2020-10-19 20:11:48 -07:00
|
|
|
Try Again
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="card-body text-center">
|
2021-11-28 12:49:22 -08:00
|
|
|
<span className="spinner-grow spinner-grow-sm me-2"></span>
|
2020-10-19 20:11:48 -07:00
|
|
|
Loading
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|