diff --git a/app/components/LiveTransactionStatsCard.tsx b/app/components/LiveTransactionStatsCard.tsx index 5d92c950..cb380855 100644 --- a/app/components/LiveTransactionStatsCard.tsx +++ b/app/components/LiveTransactionStatsCard.tsx @@ -4,21 +4,11 @@ import { TableCardBody } from '@components/common/TableCardBody'; import { StatsNotReady } from '@components/StatsNotReady'; import { ClusterStatsStatus, PERF_UPDATE_SEC, usePerformanceInfo } from '@providers/stats/solanaClusterStats'; import { PerformanceInfo } from '@providers/stats/solanaPerformanceInfo'; -import { PingInfo, PingRollupInfo, PingStatus, useSolanaPingInfo } from '@providers/stats/SolanaPingProvider'; import { BarElement, CategoryScale, Chart, ChartData, ChartOptions, LinearScale, Tooltip } from 'chart.js'; import classNames from 'classnames'; import React from 'react'; import { Bar } from 'react-chartjs-2'; import CountUp from 'react-countup'; -import { RefreshCw } from 'react-feather'; - -import { useCluster } from '../providers/cluster'; -import { - useValidatorsAppPingStats, - ValidatorsAppPingStatsInfo, - ValidatorsAppPingStatsRecord, -} from '../providers/stats/ValidatorsAppStatsProvider'; -import { Cluster } from '../utils/cluster'; Chart.register(BarElement, CategoryScale, LinearScale, Tooltip); @@ -42,18 +32,24 @@ const SERIES_INFO = { export function LiveTransactionStatsCard() { const [series, setSeries] = React.useState('short'); - const { cluster } = useCluster(); return (

Live Transaction Stats

- {cluster === Cluster.MainnetBeta ? ( - - ) : ( - - )} +
+

+ For transaction confirmation time statistics, please visit{' '} + + validators.app + {' '} + or{' '} + + solscan.io + +

+
); } @@ -68,29 +64,6 @@ function TpsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeri return ; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function ValidatorsAppTpsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeries }) { - const performanceInfo = usePerformanceInfo(); - const statsInfo = useValidatorsAppPingStats(); - - if (performanceInfo.status !== ClusterStatsStatus.Ready || statsInfo.status !== PingStatus.Ready) { - return ( - - ); - } - - return ( - - ); -} - const TPS_CHART_OPTIONS = (historyMaxTps: number): ChartOptions<'bar'> => { return { animation: false, @@ -182,84 +155,6 @@ const TPS_CHART_OPTIONS = (historyMaxTps: number): ChartOptions<'bar'> => { }; }; -type ValidatorsAppTpsBarChartProps = { - performanceInfo: PerformanceInfo; - statsInfo: ValidatorsAppPingStatsInfo; - series: Series; - setSeries: SetSeries; -}; -function ValidatorsAppTpsBarChart({ statsInfo, performanceInfo, series, setSeries }: ValidatorsAppTpsBarChartProps) { - const seriesData = statsInfo[series] || []; - const avgTps = Math.round( - (statsInfo[SERIES[2]] || [])?.map(val => val.tps).reduce((a, b) => a + b, 0) / seriesData.length - ); - const historyMaxTps = (statsInfo[SERIES[2]] || [])?.map(val => val.tps).reduce((a, b) => Math.max(a, b), 0); - const averageTps = Math.round(avgTps).toLocaleString('en-US'); - const transactionCount = ; - const chartOptions = React.useMemo>(() => TPS_CHART_OPTIONS(historyMaxTps), [historyMaxTps]); - - const now = new Date(); - const chartData: ChartData<'bar'> = { - datasets: [ - { - backgroundColor: '#00D192', - borderWidth: 0, - data: seriesData.map(val => (val.tps >= historyMaxTps ? avgTps : val.tps || 0)), - hoverBackgroundColor: '#00D192', - }, - ], - labels: seriesData.map(val => { - const minAgo = Math.round((now.getTime() - val.timestamp.getTime()) / 60000); - return `${minAgo}min ago`; - }), - }; - - return ( - <> - - - Transaction count - {transactionCount} - - - Transactions per second (TPS) - {averageTps} - - - -
- -
-
-
- TPS history - -
- {SERIES.map(key => ( - - ))} -
-
- -
-
- -
-
-
-
- - ); -} - type TpsBarChartProps = { performanceInfo: PerformanceInfo; series: Series; @@ -379,324 +274,3 @@ function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) { /> ); } - -function PingStatsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeries }) { - const pingInfo = useSolanaPingInfo(); - - if (pingInfo.status !== PingStatus.Ready) { - return ; - } - - return ; -} - -function ValidatorsAppPingStatsCardBody({ series, setSeries }: { series: Series; setSeries: SetSeries }) { - const pingInfo = useValidatorsAppPingStats(); - - if (pingInfo.status !== PingStatus.Ready) { - return ; - } - - return ; -} - -type StatsNotReadyProps = { error: boolean; retry?: () => void }; -function PingStatsNotReady({ error, retry }: StatsNotReadyProps) { - if (error) { - return ( -
- There was a problem loading solana ping stats.{' '} - {retry && ( - - )} -
- ); - } - - return ( -
- - Loading -
- ); -} - -const PING_CHART_OPTIONS: ChartOptions<'bar'> = { - animation: false, - interaction: { - intersect: false, - mode: 'index', - }, - plugins: { - tooltip: { - enabled: false, // Disable the on-canvas tooltip - external(context) { - // Tooltip Element - let tooltipEl = document.getElementById('chartjs-tooltip'); - - // Create element on first render - if (!tooltipEl) { - tooltipEl = document.createElement('div'); - tooltipEl.id = 'chartjs-tooltip'; - tooltipEl.innerHTML = '
'; - document.body.appendChild(tooltipEl); - } - - // Hide if no tooltip - const tooltipModel = context.tooltip; - if (tooltipModel.opacity === 0) { - tooltipEl.style.opacity = '0'; - return; - } - - // Set caret Position - tooltipEl.classList.remove('above', 'below', 'no-transform'); - if (tooltipModel.yAlign) { - tooltipEl.classList.add(tooltipModel.yAlign); - } else { - tooltipEl.classList.add('no-transform'); - } - - // Set Text - if (tooltipModel.body) { - const { label } = tooltipModel.dataPoints[0]; - const tooltipContent = tooltipEl.querySelector('div'); - if (tooltipContent) { - tooltipContent.innerHTML = `${label}`; - } - } - - const position = context.chart.canvas.getBoundingClientRect(); - - // Display, position, and set styles for font - tooltipEl.style.opacity = '1'; - tooltipEl.style.position = 'absolute'; - tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; - tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; - tooltipEl.style.pointerEvents = 'none'; - }, - intersect: false, - }, - }, - resizeDelay: 0, - scales: { - x: { - grid: { - display: false, - }, - ticks: { - display: false, - }, - }, - y: { - grid: { - display: false, - }, - min: 0, - ticks: { - display: true, - font: { - size: 10, - }, - stepSize: 100, - textStrokeColor: '#EEE', - }, - }, - }, -}; - -function PingBarChart({ - pingInfo, - series, - setSeries, -}: { - pingInfo: PingRollupInfo; - series: Series; - setSeries: SetSeries; -}) { - const seriesData = pingInfo[series] || []; - const maxMean = seriesData.reduce((a, b) => { - return Math.max(a, b.mean); - }, 0); - const seriesLength = seriesData.length; - const backgroundColor = (val: PingInfo) => { - if (val.submitted === 0) { - return '#08a274'; - } - - if (val.loss >= 0.25 && val.loss <= 0.5) { - return '#FFA500'; - } - - return val.loss > 0.5 ? '#f00' : '#00D192'; - }; - const chartData: ChartData<'bar'> = { - datasets: [ - { - backgroundColor: seriesData.map(backgroundColor), - borderWidth: 0, - data: seriesData.map(val => { - if (val.submitted === 0) { - return maxMean * 0.5; - } - return val.mean || 0; - }), - hoverBackgroundColor: seriesData.map(backgroundColor), - minBarLength: 2, - }, - ], - labels: seriesData.map((val, i) => { - if (val.submitted === 0) { - return ` -
-

Ping statistics unavailable

- ${SERIES_INFO[series].label(seriesLength - i)}min ago -
- `; - } - - return ` -
${val.mean} ms
-
-

${val.confirmed} of ${val.submitted} confirmed

- ${ - val.loss - ? `

${val.loss.toLocaleString(undefined, { - minimumFractionDigits: 2, - style: 'percent', - })} loss

` - : '' - } - ${SERIES_INFO[series].label(seriesLength - i)}min ago -
- `; - }), - }; - - return ( -
-
-
- Average Ping Time - -
- {SERIES.map(key => ( - - ))} -
-
- -
-
- -
-
-
-
- ); -} - -function ValidatorsAppPingBarChart({ - pingInfo, - series, - setSeries, -}: { - pingInfo: ValidatorsAppPingStatsInfo; - series: Series; - setSeries: SetSeries; -}) { - const seriesData = pingInfo[series] || []; - const maxMedian = seriesData.reduce((a, b) => { - return Math.max(a, b.median); - }, 0); - const backgroundColor = (val: ValidatorsAppPingStatsRecord) => { - if (val.submitted === 0) { - return '#08a274'; - } - - return '#00D192'; - }; - const now = new Date(); - const chartData: ChartData<'bar'> = { - datasets: [ - { - backgroundColor: seriesData.map(backgroundColor), - borderWidth: 0, - data: seriesData.map(val => { - if (val.submitted === 0) { - return maxMedian * 0.5; - } - return val.median || 0; - }), - hoverBackgroundColor: seriesData.map(backgroundColor), - minBarLength: 2, - }, - ], - labels: seriesData.map(val => { - const minutesAgo = Math.round((now.getTime() - val.timestamp.getTime()) / (60 * 1000)); - if (val.submitted === 0) { - return ` -
-

Ping statistics unavailable

- ${minutesAgo} min ago -
- `; - } - - return ` -
${val.median} ms
-
-

${val.submitted} confirmed

- ${`

${val.average_slot_latency.toLocaleString(undefined)} Average Slot Latency

`} - ${minutesAgo}min ago -
- `; - }), - }; - - return ( -
-
-
- Average Ping Time - -
- {SERIES.map(key => ( - - ))} -
-
- -
-
- -
-
-
-
- ); -} diff --git a/app/providers/stats/SolanaPingProvider.tsx b/app/providers/stats/SolanaPingProvider.tsx deleted file mode 100644 index 76b2682a..00000000 --- a/app/providers/stats/SolanaPingProvider.tsx +++ /dev/null @@ -1,168 +0,0 @@ -'use client'; - -import { useCluster } from '@providers/cluster'; -import { useStatsProvider } from '@providers/stats/solanaClusterStats'; -import { Cluster, clusterSlug } from '@utils/cluster'; -import { fetch } from 'cross-fetch'; -import React from 'react'; -import useTabVisibility from 'use-tab-visibility'; - -const FETCH_PING_INTERVAL = 60 * 1000; - -function getPingUrl(cluster: Cluster) { - const slug = clusterSlug(cluster); - - if (slug === 'custom') { - return undefined; - } - - return `https://ping.solana.com/${slug}/last6hours`; -} - -export type PingMetric = { - submitted: number; - confirmed: number; - loss: string; - mean_ms: number; - ts: string; - error: string; -}; - -export type PingInfo = { - submitted: number; - confirmed: number; - loss: number; - mean: number; - timestamp: Date; -}; - -export enum PingStatus { - Loading, - Ready, - Error, -} - -export type PingRollupInfo = { - status: PingStatus; - short?: PingInfo[]; - medium?: PingInfo[]; - long?: PingInfo[]; - retry?: () => void; -}; - -const PingContext = React.createContext(undefined); - -type Props = { children: React.ReactNode }; - -function downsample(points: PingInfo[], bucketSize: number): PingInfo[] { - const buckets = []; - - for (let start = 0; start < points.length; start += bucketSize) { - const summary: PingInfo = { - confirmed: 0, - loss: 0, - mean: 0, - submitted: 0, - timestamp: points[start].timestamp, - }; - for (let i = 0; i < bucketSize; i++) { - summary.submitted += points[start + i].submitted; - summary.confirmed += points[start + i].confirmed; - summary.mean += points[start + i].mean; - } - summary.mean = Math.round(summary.mean / bucketSize); - summary.loss = (summary.submitted - summary.confirmed) / summary.submitted; - buckets.push(summary); - } - - return buckets; -} - -export function SolanaPingProvider({ children }: Props) { - const { cluster } = useCluster(); - const { active } = useStatsProvider(); - const [rollup, setRollup] = React.useState({ - status: PingStatus.Loading, - }); - const { visible: isTabVisible } = useTabVisibility(); - React.useEffect(() => { - if (!active || !isTabVisible) { - return; - } - - const url = getPingUrl(cluster); - - setRollup({ - status: PingStatus.Loading, - }); - - if (!url) { - return; - } - let stale = false; - const fetchPingMetrics = async () => { - try { - const response = await fetch(url); - if (stale) { - return; - } - const json: PingMetric[] = await response.json(); - if (stale) { - return; - } - const points = json - .map(({ submitted, confirmed, mean_ms, ts }: PingMetric) => { - return { - confirmed, - loss: (submitted - confirmed) / submitted, - mean: mean_ms, - submitted, - timestamp: new Date(ts), - }; - }) - .reverse(); - - const short = points.slice(-30); - const medium = downsample(points, 4).slice(-30); - const long = downsample(points, 12); - - setRollup({ - long, - medium, - short, - status: PingStatus.Ready, - }); - } catch { - setRollup({ - retry: () => { - setRollup({ - status: PingStatus.Loading, - }); - - fetchPingMetrics(); - }, - status: PingStatus.Error, - }); - } - }; - - const fetchPingInterval = setInterval(() => { - fetchPingMetrics(); - }, FETCH_PING_INTERVAL); - fetchPingMetrics(); - return () => { - clearInterval(fetchPingInterval); - stale = true; - }; - }, [active, cluster, isTabVisible]); - - return {children}; -} - -export function useSolanaPingInfo() { - const context = React.useContext(PingContext); - if (!context) { - throw new Error(`useContext must be used within a StatsProvider`); - } - return context; -} diff --git a/app/providers/stats/ValidatorsAppStatsProvider.tsx b/app/providers/stats/ValidatorsAppStatsProvider.tsx deleted file mode 100644 index 816476a5..00000000 --- a/app/providers/stats/ValidatorsAppStatsProvider.tsx +++ /dev/null @@ -1,149 +0,0 @@ -'use client'; - -import { useCluster } from '@providers/cluster'; -import { useStatsProvider } from '@providers/stats/solanaClusterStats'; -import { Cluster } from '@utils/cluster'; -import { fetch } from 'cross-fetch'; -import React from 'react'; -import useTabVisibility from 'use-tab-visibility'; - -import { ValidatorsAppPingStats } from '@/app/api/ping/[network]/route'; - -const PING_INTERVALS: number[] = [1, 3, 12]; - -const FETCH_PING_INTERVAL = 60 * 1000; - -function getClusterSlug(cluster: Cluster) { - switch (cluster) { - case Cluster.MainnetBeta: - return 'mainnet'; - case Cluster.Devnet: - return 'devnet'; - default: - throw new Error(`Invalid cluster: ${cluster}`); - } -} - -function getUrl(cluster: Cluster) { - return `/api/ping/${getClusterSlug(cluster)}`; -} - -export enum PingStatus { - Loading, - Ready, - Error, -} - -export type ValidatorsAppPingStatsInfo = { - status: PingStatus; - short?: ValidatorsAppPingStatsRecord[]; - medium?: ValidatorsAppPingStatsRecord[]; - long?: ValidatorsAppPingStatsRecord[]; - retry?: () => void; -}; - -export type ValidatorsAppPingStatsRecord = { - tps: number; - median: number; - submitted: number; - average_slot_latency: number; - timestamp: Date; -}; - -const ValidatorsAppStatsContext = React.createContext(undefined); - -type Props = { children: React.ReactNode }; - -export function ValidatorsAppStatsProvider({ children }: Props) { - const { cluster } = useCluster(); - const { active } = useStatsProvider(); - const [rollup, setRollup] = React.useState({ - status: PingStatus.Loading, - }); - const { visible: isTabVisible } = useTabVisibility(); - React.useEffect(() => { - if (!active || !isTabVisible) { - return; - } - - if (cluster === Cluster.Testnet || cluster === Cluster.Custom) { - return; - } - - const url = getUrl(cluster); - - setRollup({ - status: PingStatus.Loading, - }); - - if (!url) { - return; - } - let stale = false; - const fetchPingMetrics = async () => { - try { - const response = await fetch(url); - if (stale) { - return; - } - const json: { [interval: number]: ValidatorsAppPingStats[] } = await response.json(); - if (stale) { - return; - } - const simplify = (data: ValidatorsAppPingStats[]) => - data.map( - ({ tps, median, time_from, num_of_records, average_slot_latency }: ValidatorsAppPingStats) => { - return { - average_slot_latency, - median, - submitted: num_of_records, - timestamp: new Date(time_from), - tps, - }; - } - ); - - const short = simplify(json[PING_INTERVALS[0]]).slice(-30); - const medium = simplify(json[PING_INTERVALS[1]]).slice(-30); - const long = simplify(json[PING_INTERVALS[2]]).slice(-30); - - setRollup({ - long, - medium, - short, - status: PingStatus.Ready, - }); - } catch { - setRollup({ - retry: () => { - setRollup({ - status: PingStatus.Loading, - }); - - fetchPingMetrics(); - }, - status: PingStatus.Error, - }); - } - }; - - const fetchPingInterval = setInterval(() => { - fetchPingMetrics(); - }, FETCH_PING_INTERVAL); - fetchPingMetrics(); - return () => { - clearInterval(fetchPingInterval); - stale = true; - }; - }, [active, cluster, isTabVisible]); - - return {children}; -} - -export function useValidatorsAppPingStats() { - const context = React.useContext(ValidatorsAppStatsContext); - if (!context) { - throw new Error(`useContext must be used within a StatsProvider`); - } - return context; -} diff --git a/app/providers/stats/index.tsx b/app/providers/stats/index.tsx index f3e76a3f..45c29351 100644 --- a/app/providers/stats/index.tsx +++ b/app/providers/stats/index.tsx @@ -1,16 +1,8 @@ -import { SolanaPingProvider } from '@providers/stats/SolanaPingProvider'; import React from 'react'; import { SolanaClusterStatsProvider } from './solanaClusterStats'; -import { ValidatorsAppStatsProvider } from './ValidatorsAppStatsProvider'; type Props = { children: React.ReactNode }; export function StatsProvider({ children }: Props) { - return ( - - - {children} - - - ); + return {children}; }