diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index be665a6be6..c834ce9186 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -46,8 +46,10 @@ function App() { /> } + path={["/block/:id", "/block/:id/:tab"]} + render={({ match }) => ( + + )} />
@@ -41,6 +45,12 @@ export function UpgradeableProgramSection({
+ {label && ( + + Address Label + {label} + + )} Balance (SOL) diff --git a/explorer/src/components/block/BlockAccountsCard.tsx b/explorer/src/components/block/BlockAccountsCard.tsx new file mode 100644 index 0000000000..4e98aeeed2 --- /dev/null +++ b/explorer/src/components/block/BlockAccountsCard.tsx @@ -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(); + block.transactions.forEach((tx) => { + const txSet = new Map(); + 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 ( +
+
+

Block Account Usage

+
+ +
+ + + + + + + + + + + + {accountStats + .slice(0, numDisplayed) + .map(([address, { writes, reads }]) => { + return ( + + + + + + + + ); + })} + +
AccountRead-Write CountRead-Only CountTotal Count% of Transactions
+
+
{writes}{reads}{writes + reads} + {((100 * (writes + reads)) / totalTransactions).toFixed( + 2 + )} + % +
+
+ + {accountStats.length > numDisplayed && ( +
+ +
+ )} +
+ ); +} diff --git a/explorer/src/components/block/BlockHistoryCard.tsx b/explorer/src/components/block/BlockHistoryCard.tsx index 5352c2b426..5878ee8855 100644 --- a/explorer/src/components/block/BlockHistoryCard.tsx +++ b/explorer/src/components/block/BlockHistoryCard.tsx @@ -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 ; } @@ -24,7 +28,7 @@ export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) { - {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 }) {
+ + {block.transactions.length > numDisplayed && ( +
+ +
+ )} ); } diff --git a/explorer/src/components/block/BlockOverviewCard.tsx b/explorer/src/components/block/BlockOverviewCard.tsx index fc4fe0ab7c..b93c3b7feb 100644 --- a/explorer/src/components/block/BlockOverviewCard.tsx +++ b/explorer/src/components/block/BlockOverviewCard.tsx @@ -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 }) { - - Parent Slot - - - - Blockhash @@ -59,16 +64,96 @@ export function BlockOverviewCard({ slot }: { slot: number }) { - Previous Blockhash + Parent Slot + + + + + + Parent Blockhash {block.previousBlockhash} + + Total Transactions + + {block.transactions.length} + + - - + + + ); +} + +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 ( + <> +
+
+
+
    + {TABS.map(({ title, slug, path }) => ( +
  • + + {title} + +
  • + ))} +
+
+
+
+ {tab === undefined && } + {tab === "rewards" && } + {tab === "accounts" && } + {tab === "programs" && } ); } diff --git a/explorer/src/components/block/BlockProgramsCard.tsx b/explorer/src/components/block/BlockProgramsCard.tsx new file mode 100644 index 0000000000..427547b9df --- /dev/null +++ b/explorer/src/components/block/BlockProgramsCard.tsx @@ -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(); + const ixFrequency = new Map(); + + let totalInstructions = 0; + block.transactions.forEach((tx) => { + totalInstructions += tx.transaction.instructions.length; + const programUsed = new Set(); + 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 ( + <> +
+
+

Block Program Stats

+
+ + + Unique Programs Count + + {programEntries.length} + + + + Total Instructions + + {totalInstructions} + + + +
+
+
+

Block Programs

+
+ +
+ + + + + + + + + + + + {programEntries.map(([programId, txFreq]) => { + const ixFreq = ixFrequency.get(programId) as number; + return ( + + + + + + + + ); + })} + +
ProgramTransaction Count% of TotalInstruction Count% of Total
+
+
{txFreq}{((100 * txFreq) / totalTransactions).toFixed(2)}%{ixFreq}{((100 * ixFreq) / totalInstructions).toFixed(2)}%
+
+
+ + ); +} diff --git a/explorer/src/pages/BlockDetailsPage.tsx b/explorer/src/pages/BlockDetailsPage.tsx index d01411e19b..2a4f60c091 100644 --- a/explorer/src/pages/BlockDetailsPage.tsx +++ b/explorer/src/pages/BlockDetailsPage.tsx @@ -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 = ; @@ -17,7 +17,7 @@ export function BlockDetailsPage({ slot }: Props) { slotNumber < MAX_SAFE_INTEGER && slotNumber % 1 === 0 ) { - output = ; + output = ; } return ( diff --git a/explorer/src/utils/tx.ts b/explorer/src/utils/tx.ts index 1886817dee..f18e97d5ff 100644 --- a/explorer/src/utils/tx.ts +++ b/explorer/src/utils/tx.ts @@ -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) ); } diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json index 8d11498dd3..7537f97bf4 100644 --- a/explorer/tsconfig.json +++ b/explorer/tsconfig.json @@ -4,6 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, + "downlevelIteration": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true,