From af005d79ad22d96059265bde7d0915fe17527c8b Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 12 Sep 2021 19:05:46 -0500 Subject: [PATCH] explorer: Display invoked programs in block transaction list (#19815) --- .../src/components/block/BlockHistoryCard.tsx | 225 +++++++++++++++++- 1 file changed, 217 insertions(+), 8 deletions(-) diff --git a/explorer/src/components/block/BlockHistoryCard.tsx b/explorer/src/components/block/BlockHistoryCard.tsx index f92438dc7..039113164 100644 --- a/explorer/src/components/block/BlockHistoryCard.tsx +++ b/explorer/src/components/block/BlockHistoryCard.tsx @@ -1,37 +1,138 @@ import React from "react"; -import { BlockResponse } from "@solana/web3.js"; +import { Link } from "react-router-dom"; +import { Location } from "history"; +import { + BlockResponse, + ConfirmedTransactionMeta, + TransactionSignature, + PublicKey, +} from "@solana/web3.js"; import { ErrorCard } from "components/common/ErrorCard"; import { Signature } from "components/common/Signature"; +import { Address } from "components/common/Address"; +import { useQuery } from "utils/url"; +import { useCluster } from "providers/cluster"; +import { displayAddress } from "utils/tx"; const PAGE_SIZE = 25; +const useQueryFilter = (): string => { + const query = useQuery(); + const filter = query.get("filter"); + return filter || ""; +}; + +type TransactionWithInvocations = { + index: number; + signature?: TransactionSignature; + meta: ConfirmedTransactionMeta | null; + invocations: Map; +}; + export function BlockHistoryCard({ block }: { block: BlockResponse }) { const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE); + const [showDropdown, setDropdown] = React.useState(false); + const filter = useQueryFilter(); - if (block.transactions.length === 0) { - return ; + const { transactions, invokedPrograms } = React.useMemo(() => { + const invokedPrograms = new Map(); + + const transactions: TransactionWithInvocations[] = block.transactions.map( + (tx, index) => { + let signature: TransactionSignature | undefined; + if (tx.transaction.signatures.length > 0) { + signature = tx.transaction.signatures[0]; + } + + let programIndexes = tx.transaction.message.instructions.map( + (ix) => ix.programIdIndex + ); + programIndexes.concat( + tx.meta?.innerInstructions?.flatMap((ix) => { + return ix.instructions.map((ix) => ix.programIdIndex); + }) || [] + ); + + const indexMap = new Map(); + programIndexes.forEach((programIndex) => { + const count = indexMap.get(programIndex) || 0; + indexMap.set(programIndex, count + 1); + }); + + const invocations = new Map(); + for (const [i, count] of indexMap.entries()) { + const programId = tx.transaction.message.accountKeys[i].toBase58(); + invocations.set(programId, count); + const programTransactionCount = invokedPrograms.get(programId) || 0; + invokedPrograms.set(programId, programTransactionCount + 1); + } + + return { + index, + signature, + meta: tx.meta, + invocations, + }; + } + ); + return { transactions, invokedPrograms }; + }, [block]); + + const filteredTransactions = React.useMemo(() => { + // console.log("Filter: ", filter); + // console.log("invocations", transactions); + return transactions.filter(({ invocations }) => { + if (filter === ALL_TRANSACTIONS) { + return true; + } + return invocations.has(filter); + }); + }, [transactions, filter]); + + if (filteredTransactions.length === 0) { + const errorMessage = + filter === ALL_TRANSACTIONS + ? "This block has no transactions" + : "No transactions found with this filter"; + return ; + } + + let title: string; + if (filteredTransactions.length === transactions.length) { + title = `Block Transactions (${filteredTransactions.length})`; + } else { + title = `Block Transactions`; } return (
-

Block Transactions

+

{title}

+ setDropdown((show) => !show)} + show={showDropdown} + invokedPrograms={invokedPrograms} + totalTransactionCount={transactions.length} + >
+ + - {block.transactions.slice(0, numDisplayed).map((tx, i) => { + {filteredTransactions.slice(0, numDisplayed).map((tx, i) => { let statusText; let statusClass; let signature: React.ReactNode; - if (tx.meta?.err || tx.transaction.signatures.length === 0) { + if (tx.meta?.err || !tx.signature) { statusClass = "warning"; statusText = "Failed"; } else { @@ -39,14 +140,18 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { statusText = "Success"; } - if (tx.transaction.signatures.length > 0) { + if (tx.signature) { signature = ( - + ); } + const entries = [...tx.invocations.entries()]; + entries.sort(); + return ( + + ); })} @@ -76,3 +193,95 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { ); } + +type FilterProps = { + filter: string; + toggle: () => void; + show: boolean; + invokedPrograms: Map; + totalTransactionCount: number; +}; + +const ALL_TRANSACTIONS = ""; + +type FilterOption = { + name: string; + programId: string; + transactionCount: number; +}; + +const FilterDropdown = ({ + filter, + toggle, + show, + invokedPrograms, + totalTransactionCount, +}: FilterProps) => { + const { cluster } = useCluster(); + const buildLocation = (location: Location, filter: string) => { + const params = new URLSearchParams(location.search); + if (filter === ALL_TRANSACTIONS) { + params.delete("filter"); + } else { + params.set("filter", filter); + } + return { + ...location, + search: params.toString(), + }; + }; + + let currentFilterOption = { + name: "All Transactions", + programId: ALL_TRANSACTIONS, + transactionCount: totalTransactionCount, + }; + const filterOptions: FilterOption[] = [currentFilterOption]; + const placeholderRegistry = new Map(); + + [...invokedPrograms.entries()].forEach(([programId, transactionCount]) => { + const name = displayAddress(programId, cluster, placeholderRegistry); + if (filter === programId) { + currentFilterOption = { + programId, + name: `${name} Transactions (${transactionCount})`, + transactionCount, + }; + } + filterOptions.push({ name, programId, transactionCount }); + }); + + filterOptions.sort(); + + return ( +
+ +
+ {filterOptions.map(({ name, programId, transactionCount }) => { + return ( + buildLocation(location, programId)} + className={`dropdown-item${ + programId === filter ? " active" : "" + }`} + onClick={toggle} + > + {`${name} (${transactionCount})`} + + ); + })} +
+
+ ); +};
# Result Transaction SignatureInvoked Programs
{tx.index + 1} {statusText} @@ -54,6 +159,18 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { {signature} + {tx.invocations.size === 0 + ? "NA" + : entries.map(([programId, count], i) => { + return ( +
+
+ {`(${count})`} +
+ ); + })} +