explorer: Add tabs for block program and account stats (#15702)

This commit is contained in:
Justin Starry 2021-03-04 21:12:41 +08:00 committed by GitHub
parent 7bf430b360
commit efa3cd711c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 18 deletions

View File

@ -46,8 +46,10 @@ function App() {
/>
<Route
exact
path={"/block/:id"}
render={({ match }) => <BlockDetailsPage slot={match.params.id} />}
path={["/block/:id", "/block/:id/:tab"]}
render={({ match }) => (
<BlockDetailsPage slot={match.params.id} tab={match.params.tab} />
)}
/>
<Route
exact

View File

@ -8,6 +8,8 @@ import {
ProgramDataAccountInfo,
} from "validators/accounts/upgradeable-program";
import { Slot } from "components/common/Slot";
import { addressLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
export function UpgradeableProgramSection({
account,
@ -19,6 +21,8 @@ export function UpgradeableProgramSection({
programData: ProgramDataAccountInfo;
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const label = addressLabel(account.pubkey.toBase58(), cluster);
return (
<div className="card">
<div className="card-header">
@ -41,6 +45,12 @@ export function UpgradeableProgramSection({
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-right">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">

View File

@ -0,0 +1,110 @@
import React from "react";
import { ConfirmedBlock, PublicKey } from "@solana/web3.js";
import { Address } from "components/common/Address";
type AccountStats = {
reads: number;
writes: number;
};
const PAGE_SIZE = 25;
export function BlockAccountsCard({ block }: { block: ConfirmedBlock }) {
const [numDisplayed, setNumDisplayed] = React.useState(10);
const totalTransactions = block.transactions.length;
const accountStats = React.useMemo(() => {
const statsMap = new Map<string, AccountStats>();
block.transactions.forEach((tx) => {
const txSet = new Map<string, boolean>();
tx.transaction.instructions.forEach((ix) => {
ix.keys.forEach((key) => {
const address = key.pubkey.toBase58();
txSet.set(address, key.isWritable);
});
});
txSet.forEach((isWritable, address) => {
const stats = statsMap.get(address) || { reads: 0, writes: 0 };
if (isWritable) {
stats.writes++;
} else {
stats.reads++;
}
statsMap.set(address, stats);
});
});
const accountEntries = [];
for (let entry of statsMap) {
accountEntries.push(entry);
}
accountEntries.sort((a, b) => {
const aCount = a[1].reads + a[1].writes;
const bCount = b[1].reads + b[1].writes;
if (aCount < bCount) return 1;
if (aCount > bCount) return -1;
return 0;
});
return accountEntries;
}, [block]);
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Account Usage</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Account</th>
<th className="text-muted">Read-Write Count</th>
<th className="text-muted">Read-Only Count</th>
<th className="text-muted">Total Count</th>
<th className="text-muted">% of Transactions</th>
</tr>
</thead>
<tbody>
{accountStats
.slice(0, numDisplayed)
.map(([address, { writes, reads }]) => {
return (
<tr key={address}>
<td>
<Address pubkey={new PublicKey(address)} link />
</td>
<td>{writes}</td>
<td>{reads}</td>
<td>{writes + reads}</td>
<td>
{((100 * (writes + reads)) / totalTransactions).toFixed(
2
)}
%
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{accountStats.length > numDisplayed && (
<div className="card-footer">
<button
className="btn btn-primary w-100"
onClick={() =>
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
}
>
Load More
</button>
</div>
)}
</div>
);
}

View File

@ -4,7 +4,11 @@ import { ErrorCard } from "components/common/ErrorCard";
import { Signature } from "components/common/Signature";
import bs58 from "bs58";
const PAGE_SIZE = 25;
export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE);
if (block.transactions.length === 0) {
return <ErrorCard text="This block has no transactions" />;
}
@ -24,7 +28,7 @@ export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
</tr>
</thead>
<tbody className="list">
{block.transactions.map((tx, i) => {
{block.transactions.slice(0, numDisplayed).map((tx, i) => {
let statusText;
let statusClass;
let signature: React.ReactNode;
@ -60,6 +64,19 @@ export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
</tbody>
</table>
</div>
{block.transactions.length > numDisplayed && (
<div className="card-footer">
<button
className="btn btn-primary w-100"
onClick={() =>
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
}
>
Load More
</button>
</div>
)}
</div>
);
}

View File

@ -7,8 +7,19 @@ import { Slot } from "components/common/Slot";
import { ClusterStatus, useCluster } from "providers/cluster";
import { BlockHistoryCard } from "./BlockHistoryCard";
import { BlockRewardsCard } from "./BlockRewardsCard";
import { ConfirmedBlock } from "@solana/web3.js";
import { NavLink } from "react-router-dom";
import { clusterPath } from "utils/url";
import { BlockProgramsCard } from "./BlockProgramsCard";
import { BlockAccountsCard } from "./BlockAccountsCard";
export function BlockOverviewCard({ slot }: { slot: number }) {
export function BlockOverviewCard({
slot,
tab,
}: {
slot: number;
tab?: string;
}) {
const confirmedBlock = useBlock(slot);
const fetchBlock = useFetchBlock();
const { status } = useCluster();
@ -46,12 +57,6 @@ export function BlockOverviewCard({ slot }: { slot: number }) {
<Slot slot={slot} />
</td>
</tr>
<tr>
<td className="w-100">Parent Slot</td>
<td className="text-lg-right text-monospace">
<Slot slot={block.parentSlot} link />
</td>
</tr>
<tr>
<td className="w-100">Blockhash</td>
<td className="text-lg-right text-monospace">
@ -59,16 +64,96 @@ export function BlockOverviewCard({ slot }: { slot: number }) {
</td>
</tr>
<tr>
<td className="w-100">Previous Blockhash</td>
<td className="w-100">Parent Slot</td>
<td className="text-lg-right text-monospace">
<Slot slot={block.parentSlot} link />
</td>
</tr>
<tr>
<td className="w-100">Parent Blockhash</td>
<td className="text-lg-right text-monospace">
<span>{block.previousBlockhash}</span>
</td>
</tr>
<tr>
<td className="w-100">Total Transactions</td>
<td className="text-lg-right text-monospace">
<span>{block.transactions.length}</span>
</td>
</tr>
</TableCardBody>
</div>
<BlockRewardsCard block={block} />
<BlockHistoryCard block={block} />
<MoreSection block={block} slot={slot} tab={tab} />
</>
);
}
const TABS: Tab[] = [
{
slug: "history",
title: "Transactions",
path: "",
},
{
slug: "rewards",
title: "Rewards",
path: "/rewards",
},
{
slug: "programs",
title: "Programs",
path: "/programs",
},
{
slug: "accounts",
title: "Accounts",
path: "/accounts",
},
];
type MoreTabs = "history" | "rewards" | "programs" | "accounts";
type Tab = {
slug: MoreTabs;
title: string;
path: string;
};
function MoreSection({
slot,
block,
tab,
}: {
slot: number;
block: ConfirmedBlock;
tab?: string;
}) {
return (
<>
<div className="container">
<div className="header">
<div className="header-body pt-0">
<ul className="nav nav-tabs nav-overflow header-tabs">
{TABS.map(({ title, slug, path }) => (
<li key={slug} className="nav-item">
<NavLink
className="nav-link"
to={clusterPath(`/block/${slot}${path}`)}
exact
>
{title}
</NavLink>
</li>
))}
</ul>
</div>
</div>
</div>
{tab === undefined && <BlockHistoryCard block={block} />}
{tab === "rewards" && <BlockRewardsCard block={block} />}
{tab === "accounts" && <BlockAccountsCard block={block} />}
{tab === "programs" && <BlockProgramsCard block={block} />}
</>
);
}

View File

@ -0,0 +1,109 @@
import React from "react";
import { ConfirmedBlock, PublicKey } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { TableCardBody } from "components/common/TableCardBody";
export function BlockProgramsCard({ block }: { block: ConfirmedBlock }) {
const totalTransactions = block.transactions.length;
const txFrequency = new Map<string, number>();
const ixFrequency = new Map<string, number>();
let totalInstructions = 0;
block.transactions.forEach((tx) => {
totalInstructions += tx.transaction.instructions.length;
const programUsed = new Set<string>();
const trackProgramId = (programId: PublicKey) => {
const programAddress = programId.toBase58();
programUsed.add(programAddress);
const frequency = ixFrequency.get(programAddress);
ixFrequency.set(programAddress, frequency ? frequency + 1 : 1);
};
tx.transaction.instructions.forEach((ix, index) => {
trackProgramId(ix.programId);
tx.meta?.innerInstructions?.forEach((inner) => {
if (inner.index !== index) return;
totalInstructions += inner.instructions.length;
inner.instructions.forEach((innerIx) => {
if (innerIx.programIdIndex >= ix.keys.length) return;
trackProgramId(ix.keys[innerIx.programIdIndex].pubkey);
});
});
});
programUsed.forEach((programId) => {
const frequency = txFrequency.get(programId);
txFrequency.set(programId, frequency ? frequency + 1 : 1);
});
});
const programEntries = [];
for (let entry of txFrequency) {
programEntries.push(entry);
}
programEntries.sort((a, b) => {
if (a[1] < b[1]) return 1;
if (a[1] > b[1]) return -1;
return 0;
});
return (
<>
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Program Stats</h3>
</div>
<TableCardBody>
<tr>
<td className="w-100">Unique Programs Count</td>
<td className="text-lg-right text-monospace">
{programEntries.length}
</td>
</tr>
<tr>
<td className="w-100">Total Instructions</td>
<td className="text-lg-right text-monospace">
{totalInstructions}
</td>
</tr>
</TableCardBody>
</div>
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Programs</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Program</th>
<th className="text-muted">Transaction Count</th>
<th className="text-muted">% of Total</th>
<th className="text-muted">Instruction Count</th>
<th className="text-muted">% of Total</th>
</tr>
</thead>
<tbody>
{programEntries.map(([programId, txFreq]) => {
const ixFreq = ixFrequency.get(programId) as number;
return (
<tr key={programId}>
<td>
<Address pubkey={new PublicKey(programId)} link />
</td>
<td>{txFreq}</td>
<td>{((100 * txFreq) / totalTransactions).toFixed(2)}%</td>
<td>{ixFreq}</td>
<td>{((100 * ixFreq) / totalInstructions).toFixed(2)}%</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</>
);
}

View File

@ -6,9 +6,9 @@ import { BlockOverviewCard } from "components/block/BlockOverviewCard";
// IE11 doesn't support Number.MAX_SAFE_INTEGER
const MAX_SAFE_INTEGER = 9007199254740991;
type Props = { slot: string };
type Props = { slot: string; tab?: string };
export function BlockDetailsPage({ slot }: Props) {
export function BlockDetailsPage({ slot, tab }: Props) {
const slotNumber = Number(slot);
let output = <ErrorCard text={`Block ${slot} is not valid`} />;
@ -17,7 +17,7 @@ export function BlockDetailsPage({ slot }: Props) {
slotNumber < MAX_SAFE_INTEGER &&
slotNumber % 1 === 0
) {
output = <BlockOverviewCard slot={slotNumber} />;
output = <BlockOverviewCard slot={slotNumber} tab={tab} />;
}
return (

View File

@ -109,14 +109,14 @@ export const SYSVAR_IDS = {
export function addressLabel(
address: string,
cluster: Cluster,
tokenRegistry: KnownTokenMap
tokenRegistry?: KnownTokenMap
): string | undefined {
return (
PROGRAM_NAME_BY_ID[address] ||
LOADER_IDS[address] ||
SYSVAR_IDS[address] ||
SYSVAR_ID[address] ||
tokenRegistry.get(address)?.tokenName ||
tokenRegistry?.get(address)?.tokenName ||
SerumMarketRegistry.get(address, cluster)
);
}

View File

@ -4,6 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,