explorer: Add tabs for block program and account stats (#15702)
This commit is contained in:
parent
7bf430b360
commit
efa3cd711c
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
|
|
Loading…
Reference in New Issue