diff --git a/explorer/src/components/block/BlockHistoryCard.tsx b/explorer/src/components/block/BlockHistoryCard.tsx index 6c3a39cfb1..7eb2b0706b 100644 --- a/explorer/src/components/block/BlockHistoryCard.tsx +++ b/explorer/src/components/block/BlockHistoryCard.tsx @@ -6,6 +6,7 @@ import { ConfirmedTransactionMeta, TransactionSignature, PublicKey, + VOTE_PROGRAM_ID, } from "@solana/web3.js"; import { ErrorCard } from "components/common/ErrorCard"; import { Signature } from "components/common/Signature"; @@ -13,6 +14,7 @@ import { Address } from "components/common/Address"; import { useQuery } from "utils/url"; import { useCluster } from "providers/cluster"; import { displayAddress } from "utils/tx"; +import { parseProgramLogs } from "utils/program-logs"; const PAGE_SIZE = 25; @@ -27,12 +29,15 @@ type TransactionWithInvocations = { signature?: TransactionSignature; meta: ConfirmedTransactionMeta | null; invocations: Map; + computeUnits: number; + logTruncated: boolean; }; export function BlockHistoryCard({ block }: { block: BlockResponse }) { const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE); const [showDropdown, setDropdown] = React.useState(false); const filter = useQueryFilter(); + const { cluster } = useCluster(); const { transactions, invokedPrograms } = React.useMemo(() => { const invokedPrograms = new Map(); @@ -44,14 +49,13 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { 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); - }) || [] - ); + let programIndexes = tx.transaction.message.instructions + .map((ix) => ix.programIdIndex) + .concat( + tx.meta?.innerInstructions?.flatMap((ix) => { + return ix.instructions.map((ix) => ix.programIdIndex); + }) || [] + ); const indexMap = new Map(); programIndexes.forEach((programIndex) => { @@ -67,21 +71,38 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { invokedPrograms.set(programId, programTransactionCount + 1); } + const parsedLogs = parseProgramLogs( + tx.meta?.logMessages ?? [], + tx.meta?.err ?? null, + cluster + ); + + const logTruncated = parsedLogs[parsedLogs.length - 1].truncated; + const computeUnits = parsedLogs + .map(({ computeUnits }) => computeUnits) + .reduce((sum, next) => sum + next); + return { index, signature, meta: tx.meta, invocations, + computeUnits, + logTruncated, }; } ); return { transactions, invokedPrograms }; - }, [block]); + }, [block, cluster]); const filteredTransactions = React.useMemo(() => { + const voteFilter = VOTE_PROGRAM_ID.toBase58(); return transactions.filter(({ invocations }) => { if (filter === ALL_TRANSACTIONS) { return true; + } else if (filter === HIDE_VOTES) { + // hide vote txs that don't invoke any other programs + return !(invocations.size === 1 || invocations.has(voteFilter)); } return invocations.has(filter); }); @@ -122,6 +143,7 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { # Result Transaction Signature + Compute Invoked Programs @@ -157,6 +179,10 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { {signature} + + {tx.logTruncated && ">"} + {new Intl.NumberFormat("en-US").format(tx.computeUnits)} + {tx.invocations.size === 0 ? "NA" @@ -200,7 +226,8 @@ type FilterProps = { totalTransactionCount: number; }; -const ALL_TRANSACTIONS = ""; +const ALL_TRANSACTIONS = "all"; +const HIDE_VOTES = ""; type FilterOption = { name: string; @@ -218,7 +245,7 @@ const FilterDropdown = ({ const { cluster } = useCluster(); const buildLocation = (location: Location, filter: string) => { const params = new URLSearchParams(location.search); - if (filter === ALL_TRANSACTIONS) { + if (filter === HIDE_VOTES) { params.delete("filter"); } else { params.set("filter", filter); @@ -229,12 +256,27 @@ const FilterDropdown = ({ }; }; - let currentFilterOption = { + let defaultFilterOption: FilterOption = { + name: "All Except Votes", + programId: HIDE_VOTES, + transactionCount: + totalTransactionCount - + (invokedPrograms.get(VOTE_PROGRAM_ID.toBase58()) || 0), + }; + + let allTransactionsOption: FilterOption = { name: "All Transactions", programId: ALL_TRANSACTIONS, transactionCount: totalTransactionCount, }; - const filterOptions: FilterOption[] = [currentFilterOption]; + + let currentFilterOption = + filter !== ALL_TRANSACTIONS ? defaultFilterOption : allTransactionsOption; + + const filterOptions: FilterOption[] = [ + defaultFilterOption, + allTransactionsOption, + ]; const placeholderRegistry = new Map(); [...invokedPrograms.entries()].forEach(([programId, transactionCount]) => { diff --git a/explorer/src/components/transaction/ProgramLogSection.tsx b/explorer/src/components/transaction/ProgramLogSection.tsx index 0e4eea01fa..0f8f44e85f 100644 --- a/explorer/src/components/transaction/ProgramLogSection.tsx +++ b/explorer/src/components/transaction/ProgramLogSection.tsx @@ -2,7 +2,7 @@ import React from "react"; import { SignatureProps } from "pages/TransactionDetailsPage"; import { useTransactionDetails } from "providers/transactions"; import { ProgramLogsCardBody } from "components/ProgramLogsCardBody"; -import { prettyProgramLogs } from "utils/program-logs"; +import { parseProgramLogs } from "utils/program-logs"; import { useCluster } from "providers/cluster"; export function ProgramLogSection({ signature }: SignatureProps) { @@ -18,7 +18,7 @@ export function ProgramLogSection({ signature }: SignatureProps) { let prettyLogs = null; if (logMessages !== null) { - prettyLogs = prettyProgramLogs(logMessages, err, cluster); + prettyLogs = parseProgramLogs(logMessages, err, cluster); } return ( diff --git a/explorer/src/pages/inspector/SimulatorCard.tsx b/explorer/src/pages/inspector/SimulatorCard.tsx index c0d43cc6ed..4ca2fbf66b 100644 --- a/explorer/src/pages/inspector/SimulatorCard.tsx +++ b/explorer/src/pages/inspector/SimulatorCard.tsx @@ -2,7 +2,7 @@ import React from "react"; import bs58 from "bs58"; import { Connection, Message, Transaction } from "@solana/web3.js"; import { useCluster } from "providers/cluster"; -import { InstructionLogs, prettyProgramLogs } from "utils/program-logs"; +import { InstructionLogs, parseProgramLogs } from "utils/program-logs"; import { ProgramLogsCardBody } from "components/ProgramLogsCardBody"; const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0)); @@ -111,7 +111,7 @@ function useSimulator(message: Message) { } // Prettify logs - setLogs(prettyProgramLogs(resp.value.logs, resp.value.err, cluster)); + setLogs(parseProgramLogs(resp.value.logs, resp.value.err, cluster)); } catch (err) { console.error(err); setLogs(null); diff --git a/explorer/src/utils/program-logs.ts b/explorer/src/utils/program-logs.ts index 7346c680a8..e9771b2fdd 100644 --- a/explorer/src/utils/program-logs.ts +++ b/explorer/src/utils/program-logs.ts @@ -12,10 +12,12 @@ export type LogMessage = { export type InstructionLogs = { invokedProgram: string | null; logs: LogMessage[]; + computeUnits: number; + truncated: boolean; failed: boolean; }; -export function prettyProgramLogs( +export function parseProgramLogs( logs: string[], error: TransactionError | null, cluster: Cluster @@ -44,6 +46,8 @@ export function prettyProgramLogs( text: log, style: "muted", }); + } else if (log.startsWith("Log truncated")) { + prettyLogs[prettyLogs.length - 1].truncated = true; } else { const regex = /Program (\w*) invoke \[(\d)\]/g; const matches = [...log.matchAll(regex)]; @@ -56,7 +60,9 @@ export function prettyProgramLogs( prettyLogs.push({ invokedProgram: programAddress, logs: [], + computeUnits: 0, failed: false, + truncated: false, }); } else { prettyLogs[prettyLogs.length - 1].logs.push({ @@ -88,15 +94,27 @@ export function prettyProgramLogs( prettyLogs.push({ invokedProgram: null, logs: [], + computeUnits: 0, failed: false, + truncated: false, }); depth++; } // Remove redundant program address from logs - log = log.replace(/Program \w* consumed (.*)/g, (match, p1) => { - return `Program consumed: ${p1}`; - }); + log = log.replace( + /Program \w* consumed (\d*) (.*)/g, + (match, p1, p2) => { + // Only aggregate compute units consumed from top-level tx instructions + // because they include inner ix compute units as well. + if (depth === 1) { + prettyLogs[prettyLogs.length - 1].computeUnits += + Number.parseInt(p1); + } + + return `Program consumed: ${p1} ${p2}`; + } + ); // native program logs don't start with "Program log:" prettyLogs[prettyLogs.length - 1].logs.push({ @@ -114,7 +132,9 @@ export function prettyProgramLogs( prettyLogs.push({ invokedProgram: null, logs: [], + computeUnits: 0, failed: true, + truncated: false, }); }