Skip to content

Commit

Permalink
Use experimental web3js for homepage data (#256)
Browse files Browse the repository at this point in the history
This PR is a first effort to start proving out the experimental web3js
on Explorer. All data displayed on the homepage is fetched using
functions of the experimental web3js.

Notes:

- Annoyingly this doesn't eliminate the old web3js dependency from the
homepage. There's a big tree of providers with various web3.js (and
other) dependencies in [the app
layout](https://github.com/solana-labs/explorer/blob/master/app/layout.tsx).

- The main difference for the functions covered here is using bigint in
the new web3js. I've tried to keep processing of bigint as bigint, for
example calculating percentages of bigints is done without converting
them to numbers. I've mostly left display components as number, for
example `<Slot>` is used in many places and is left unchanged, and
`<CountUp>` is a 3rd party dependency which expects numbers.

- I've added the experimental web3js using an alias
`web3js-experimental`. pnpm has an issue where peerDependencies (some
libs have web3js as a peer dependency) don't work correctly, [see issue
+ workaround used
here](pnpm/pnpm#6588 (comment))

- We mostly used the exported return types from web3js as our data
structures. The experimental web3js doesn't export return types, so I've
created minimal types that just contain the fields that we need and then
passed these around as needed

---------

Co-authored-by: steveluscher <[email protected]>
  • Loading branch information
mcintyre94 and steveluscher authored Jul 7, 2023
1 parent 411d876 commit 280a929
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 98 deletions.
14 changes: 7 additions & 7 deletions app/components/LiveTransactionStatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
const countUpRef = React.useRef({ lastUpdate: 0, period: 0, start: 0 });
const countUp = countUpRef.current;

const { transactionCount: txCount, avgTps } = info;
const { transactionCount, avgTps } = info;
const txCount = Number(transactionCount);

// Track last tx count to reset count up options
if (txCount !== txCountRef.current) {
Expand Down Expand Up @@ -442,14 +443,13 @@ function PingBarChart({
<div class="value">${val.mean} ms</div>
<div class="label">
<p class="mb-0">${val.confirmed} of ${val.submitted} confirmed</p>
${
val.loss
${val.loss
? `<p class="mb-0">${val.loss.toLocaleString(undefined, {
minimumFractionDigits: 2,
style: 'percent',
})} loss</p>`
minimumFractionDigits: 2,
style: 'percent',
})} loss</p>`
: ''
}
}
${SERIES_INFO[series].label(seriesLength - i)}min ago
</div>
`;
Expand Down
8 changes: 5 additions & 3 deletions app/components/TopAccountsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import React, { createRef, useMemo } from 'react';
import { ChevronDown } from 'react-feather';
import useAsyncEffect from 'use-async-effect';

import { percentage } from '../utils/math';

type Filter = 'circulating' | 'nonCirculating' | 'all' | null;

export function TopAccountsCard() {
Expand All @@ -33,7 +35,7 @@ export function TopAccountsCard() {
return <ErrorCard text={richList} retry={fetchRichList} />;
}

let supplyCount: number;
let supplyCount: bigint;
let accounts, header;

if (richList !== Status.Idle) {
Expand Down Expand Up @@ -105,7 +107,7 @@ export function TopAccountsCard() {
);
}

const renderAccountRow = (account: AccountBalancePair, index: number, supply: number) => {
const renderAccountRow = (account: AccountBalancePair, index: number, supply: bigint) => {
return (
<tr key={index}>
<td>
Expand All @@ -117,7 +119,7 @@ const renderAccountRow = (account: AccountBalancePair, index: number, supply: nu
<td className="text-end">
<SolBalance lamports={account.lamports} maximumFractionDigits={0} />
</td>
<td className="text-end">{`${((100 * account.lamports) / supply).toFixed(3)}%`}</td>
<td className="text-end">{percentage(BigInt(100 * account.lamports), supply, 4).toFixed(3) + '%'}</td>
</tr>
);
};
Expand Down
24 changes: 13 additions & 11 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { Status, useFetchSupply, useSupply } from '@providers/supply';
import { ClusterStatus } from '@utils/cluster';
import { abbreviatedNumber, lamportsToSol, slotsToHumanString } from '@utils/index';
import { percentage } from '@utils/math';
import React from 'react';

export default function Page() {
Expand Down Expand Up @@ -59,13 +60,13 @@ function StakingComponent() {

const delinquentStake = React.useMemo(() => {
if (voteAccounts) {
return voteAccounts.delinquent.reduce((prev, current) => prev + current.activatedStake, 0);
return voteAccounts.delinquent.reduce((prev, current) => prev + current.activatedStake, BigInt(0));
}
}, [voteAccounts]);

const activeStake = React.useMemo(() => {
if (voteAccounts && delinquentStake) {
return voteAccounts.current.reduce((prev, current) => prev + current.activatedStake, 0) + delinquentStake;
return voteAccounts.current.reduce((prev, current) => prev + current.activatedStake, BigInt(0)) + delinquentStake;
}
}, [voteAccounts, delinquentStake]);

Expand All @@ -80,11 +81,12 @@ function StakingComponent() {
return <ErrorCard text={supply} retry={fetchData} />;
}

const circulatingPercentage = ((supply.circulating / supply.total) * 100).toFixed(1);
// Calculate to 2dp for accuracy, then display as 1
const circulatingPercentage = percentage(supply.circulating, supply.total, 2).toFixed(1);

let delinquentStakePercentage;
if (delinquentStake && activeStake) {
delinquentStakePercentage = ((delinquentStake / activeStake) * 100).toFixed(1);
delinquentStakePercentage = percentage(delinquentStake, activeStake, 2).toFixed(1);
}

return (
Expand All @@ -107,11 +109,11 @@ function StakingComponent() {
<div className="card">
<div className="card-body">
<h4>Active Stake</h4>
{activeStake && (
{activeStake ? (
<h1>
<em>{displayLamports(activeStake)}</em> / <small>{displayLamports(supply.total)}</small>
</h1>
)}
) : null}
{delinquentStakePercentage && (
<h5>
Delinquent stake: <em>{delinquentStakePercentage}%</em>
Expand All @@ -124,7 +126,7 @@ function StakingComponent() {
);
}

function displayLamports(value: number) {
function displayLamports(value: number | bigint) {
return abbreviatedNumber(lamportsToSol(value));
}

Expand All @@ -149,23 +151,23 @@ function StatsCardBody() {
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
const { slotIndex, slotsInEpoch } = epochInfo;
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + '%';
const epochTimeRemaining = slotsToHumanString(slotsInEpoch - slotIndex, hourlySlotTime);
const epochProgress = percentage(slotIndex, slotsInEpoch, 2).toFixed(1) + '%';
const epochTimeRemaining = slotsToHumanString(Number(slotsInEpoch - slotIndex), hourlySlotTime);
const { blockHeight, absoluteSlot } = epochInfo;

return (
<TableCardBody>
<tr>
<td className="w-100">Slot</td>
<td className="text-lg-end font-monospace">
<Slot slot={absoluteSlot} link />
<Slot slot={Number(absoluteSlot)} link />
</td>
</tr>
{blockHeight !== undefined && (
<tr>
<td className="w-100">Block height</td>
<td className="text-lg-end font-monospace">
<Slot slot={blockHeight} />
<Slot slot={Number(blockHeight)} />
</td>
</tr>
)}
Expand Down
28 changes: 22 additions & 6 deletions app/providers/accounts/vote-accounts.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { useCluster } from '@providers/cluster';
import { Connection, VoteAccountStatus } from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { reportError } from '@utils/sentry';
import React from 'react';
import { createDefaultRpcTransport, createSolanaRpc } from 'web3js-experimental';

type VoteAccountInfo = Readonly<{
activatedStake: bigint,
}>;

type VoteAccounts = Readonly<{
current: VoteAccountInfo[],
delinquent: VoteAccountInfo[],
}>;

async function fetchVoteAccounts(
cluster: Cluster,
url: string,
setVoteAccounts: React.Dispatch<React.SetStateAction<VoteAccountStatus | undefined>>
setVoteAccounts: React.Dispatch<React.SetStateAction<VoteAccounts | undefined>>
) {
try {
const connection = new Connection(url);
const result = await connection.getVoteAccounts();
setVoteAccounts(result);
const transport = createDefaultRpcTransport({ url });
const rpc = createSolanaRpc({ transport });

const voteAccountsResponse = await rpc.getVoteAccounts({ commitment: 'confirmed' }).send();
const voteAccounts: VoteAccounts = {
current: voteAccountsResponse.current.map(c => ({ activatedStake: c.activatedStake })),
delinquent: voteAccountsResponse.delinquent.map(d => ({ activatedStake: d.activatedStake })),
}

setVoteAccounts(voteAccounts);
} catch (error) {
if (cluster !== Cluster.Custom) {
reportError(error, { url });
Expand All @@ -21,7 +37,7 @@ async function fetchVoteAccounts(
}

export function useVoteAccounts() {
const [voteAccounts, setVoteAccounts] = React.useState<VoteAccountStatus>();
const [voteAccounts, setVoteAccounts] = React.useState<VoteAccounts>();
const { cluster, url } = useCluster();

return {
Expand Down
86 changes: 46 additions & 40 deletions app/providers/stats/solanaClusterStats.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client';

import { useCluster } from '@providers/cluster';
import { Connection } from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { reportError } from '@utils/sentry';
import React from 'react';
import useTabVisibility from 'use-tab-visibility';
import { createDefaultRpcTransport, createSolanaRpc } from 'web3js-experimental';

import { DashboardInfo, DashboardInfoActionType, dashboardInfoReducer } from './solanaDashboardInfo';
import { PerformanceInfo, PerformanceInfoActionType, performanceInfoReducer } from './solanaPerformanceInfo';
import { DashboardInfo, DashboardInfoActionType, dashboardInfoReducer, EpochInfo } from './solanaDashboardInfo';
import { PerformanceInfo, PerformanceInfoActionType, performanceInfoReducer, PerformanceSample } from './solanaPerformanceInfo';

export const PERF_UPDATE_SEC = 5;
export const SAMPLE_HISTORY_HOURS = 6;
Expand All @@ -33,30 +33,30 @@ const initialPerformanceInfo: PerformanceInfo = {
short: [],
},
status: ClusterStatsStatus.Loading,
transactionCount: 0,
transactionCount: BigInt(0),
};

const initialDashboardInfo: DashboardInfo = {
avgSlotTime_1h: 0,
avgSlotTime_1min: 0,
epochInfo: {
absoluteSlot: 0,
blockHeight: 0,
epoch: 0,
slotIndex: 0,
slotsInEpoch: 0,
absoluteSlot: BigInt(0),
blockHeight: BigInt(0),
epoch: BigInt(0),
slotIndex: BigInt(0),
slotsInEpoch: BigInt(0),
},
status: ClusterStatsStatus.Loading,
};

type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
const StatsProviderContext = React.createContext<
| {
setActive: SetActive;
setTimedOut: () => void;
retry: () => void;
active: boolean;
}
setActive: SetActive;
setTimedOut: () => void;
retry: () => void;
active: boolean;
}
| undefined
>(undefined);

Expand All @@ -68,14 +68,6 @@ const PerformanceContext = React.createContext<PerformanceState | undefined>(und

type Props = { children: React.ReactNode };

function getConnection(url: string): Connection | undefined {
try {
return new Connection(url);
} catch (error) {
/* empty */
}
}

export function SolanaClusterStatsProvider({ children }: Props) {
const { cluster, url } = useCluster();
const [active, setActive] = React.useState(false);
Expand All @@ -85,19 +77,25 @@ export function SolanaClusterStatsProvider({ children }: Props) {
React.useEffect(() => {
if (!active || !isTabVisible || !url) return;

const connection = getConnection(url);
const transport = createDefaultRpcTransport({ url });
const rpc = createSolanaRpc({ transport });

if (!connection) return;

let lastSlot: number | null = null;
let lastSlot: bigint | null = null;
let stale = false;
const getPerformanceSamples = async () => {
try {
const samples = await connection.getRecentPerformanceSamples(60 * SAMPLE_HISTORY_HOURS);
const samplesResponse = await rpc.getRecentPerformanceSamples(60 * SAMPLE_HISTORY_HOURS).send();

const samples: PerformanceSample[] = samplesResponse.map(s => ({
numSlots: s.numSlots,
numTransactions: s.numTransactions,
samplePeriodSecs: s.samplePeriodSecs,
}));

if (stale) {
return;
}
if (samples.length < 1) {
if (samplesResponse.length < 1) {
// no samples to work with (node has no history).
return; // we will allow for a timeout instead of throwing an error
}
Expand Down Expand Up @@ -131,7 +129,7 @@ export function SolanaClusterStatsProvider({ children }: Props) {

const getTransactionCount = async () => {
try {
const transactionCount = await connection.getTransactionCount();
const transactionCount = await rpc.getTransactionCount({ commitment: 'confirmed' }).send();
if (stale) {
return;
}
Expand All @@ -155,7 +153,16 @@ export function SolanaClusterStatsProvider({ children }: Props) {

const getEpochInfo = async () => {
try {
const epochInfo = await connection.getEpochInfo();
const epochInfoResponse = await rpc.getEpochInfo().send();

const epochInfo: EpochInfo = {
absoluteSlot: epochInfoResponse.absoluteSlot,
blockHeight: epochInfoResponse.blockHeight,
epoch: epochInfoResponse.epoch,
slotIndex: epochInfoResponse.slotIndex,
slotsInEpoch: epochInfoResponse.slotsInEpoch,
}

if (stale) {
return;
}
Expand All @@ -181,19 +188,18 @@ export function SolanaClusterStatsProvider({ children }: Props) {
const getBlockTime = async () => {
if (lastSlot) {
try {
const blockTime = await connection.getBlockTime(lastSlot);
const blockTime = await rpc.getBlockTime(lastSlot).send();

if (stale) {
return;
}
if (blockTime !== null) {
dispatchDashboardInfo({
data: {
blockTime: blockTime * 1000,
slot: lastSlot,
},
type: DashboardInfoActionType.SetLastBlockTime,
});
}
dispatchDashboardInfo({
data: {
blockTime: blockTime * 1000,
slot: lastSlot,
},
type: DashboardInfoActionType.SetLastBlockTime,
});
} catch (error) {
// let this fail gracefully
}
Expand Down
Loading

1 comment on commit 280a929

@vercel
Copy link

@vercel vercel bot commented on 280a929 Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.