explorer: Improvements to block transactions list (#24785)

* explorer: Show CPI programs in block transactions list

* explorer: Hide votes from block transactions and display compute col

* explorer: Add greater than sign to compute if tx logs truncated
This commit is contained in:
Justin Starry 2022-04-28 18:15:01 +08:00 committed by GitHub
parent b730d1da00
commit 1d6df736a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 83 additions and 21 deletions

View File

@ -6,6 +6,7 @@ import {
ConfirmedTransactionMeta, ConfirmedTransactionMeta,
TransactionSignature, TransactionSignature,
PublicKey, PublicKey,
VOTE_PROGRAM_ID,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { ErrorCard } from "components/common/ErrorCard"; import { ErrorCard } from "components/common/ErrorCard";
import { Signature } from "components/common/Signature"; import { Signature } from "components/common/Signature";
@ -13,6 +14,7 @@ import { Address } from "components/common/Address";
import { useQuery } from "utils/url"; import { useQuery } from "utils/url";
import { useCluster } from "providers/cluster"; import { useCluster } from "providers/cluster";
import { displayAddress } from "utils/tx"; import { displayAddress } from "utils/tx";
import { parseProgramLogs } from "utils/program-logs";
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
@ -27,12 +29,15 @@ type TransactionWithInvocations = {
signature?: TransactionSignature; signature?: TransactionSignature;
meta: ConfirmedTransactionMeta | null; meta: ConfirmedTransactionMeta | null;
invocations: Map<string, number>; invocations: Map<string, number>;
computeUnits: number;
logTruncated: boolean;
}; };
export function BlockHistoryCard({ block }: { block: BlockResponse }) { export function BlockHistoryCard({ block }: { block: BlockResponse }) {
const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE); const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE);
const [showDropdown, setDropdown] = React.useState(false); const [showDropdown, setDropdown] = React.useState(false);
const filter = useQueryFilter(); const filter = useQueryFilter();
const { cluster } = useCluster();
const { transactions, invokedPrograms } = React.useMemo(() => { const { transactions, invokedPrograms } = React.useMemo(() => {
const invokedPrograms = new Map<string, number>(); const invokedPrograms = new Map<string, number>();
@ -44,14 +49,13 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
signature = tx.transaction.signatures[0]; signature = tx.transaction.signatures[0];
} }
let programIndexes = tx.transaction.message.instructions.map( let programIndexes = tx.transaction.message.instructions
(ix) => ix.programIdIndex .map((ix) => ix.programIdIndex)
); .concat(
programIndexes.concat( tx.meta?.innerInstructions?.flatMap((ix) => {
tx.meta?.innerInstructions?.flatMap((ix) => { return ix.instructions.map((ix) => ix.programIdIndex);
return ix.instructions.map((ix) => ix.programIdIndex); }) || []
}) || [] );
);
const indexMap = new Map<number, number>(); const indexMap = new Map<number, number>();
programIndexes.forEach((programIndex) => { programIndexes.forEach((programIndex) => {
@ -67,21 +71,38 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
invokedPrograms.set(programId, programTransactionCount + 1); 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 { return {
index, index,
signature, signature,
meta: tx.meta, meta: tx.meta,
invocations, invocations,
computeUnits,
logTruncated,
}; };
} }
); );
return { transactions, invokedPrograms }; return { transactions, invokedPrograms };
}, [block]); }, [block, cluster]);
const filteredTransactions = React.useMemo(() => { const filteredTransactions = React.useMemo(() => {
const voteFilter = VOTE_PROGRAM_ID.toBase58();
return transactions.filter(({ invocations }) => { return transactions.filter(({ invocations }) => {
if (filter === ALL_TRANSACTIONS) { if (filter === ALL_TRANSACTIONS) {
return true; 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); return invocations.has(filter);
}); });
@ -122,6 +143,7 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
<th className="text-muted">#</th> <th className="text-muted">#</th>
<th className="text-muted">Result</th> <th className="text-muted">Result</th>
<th className="text-muted">Transaction Signature</th> <th className="text-muted">Transaction Signature</th>
<th className="text-muted">Compute</th>
<th className="text-muted">Invoked Programs</th> <th className="text-muted">Invoked Programs</th>
</tr> </tr>
</thead> </thead>
@ -157,6 +179,10 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
</td> </td>
<td>{signature}</td> <td>{signature}</td>
<td className="text-end">
{tx.logTruncated && ">"}
{new Intl.NumberFormat("en-US").format(tx.computeUnits)}
</td>
<td> <td>
{tx.invocations.size === 0 {tx.invocations.size === 0
? "NA" ? "NA"
@ -200,7 +226,8 @@ type FilterProps = {
totalTransactionCount: number; totalTransactionCount: number;
}; };
const ALL_TRANSACTIONS = ""; const ALL_TRANSACTIONS = "all";
const HIDE_VOTES = "";
type FilterOption = { type FilterOption = {
name: string; name: string;
@ -218,7 +245,7 @@ const FilterDropdown = ({
const { cluster } = useCluster(); const { cluster } = useCluster();
const buildLocation = (location: Location, filter: string) => { const buildLocation = (location: Location, filter: string) => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
if (filter === ALL_TRANSACTIONS) { if (filter === HIDE_VOTES) {
params.delete("filter"); params.delete("filter");
} else { } else {
params.set("filter", filter); 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", name: "All Transactions",
programId: ALL_TRANSACTIONS, programId: ALL_TRANSACTIONS,
transactionCount: totalTransactionCount, transactionCount: totalTransactionCount,
}; };
const filterOptions: FilterOption[] = [currentFilterOption];
let currentFilterOption =
filter !== ALL_TRANSACTIONS ? defaultFilterOption : allTransactionsOption;
const filterOptions: FilterOption[] = [
defaultFilterOption,
allTransactionsOption,
];
const placeholderRegistry = new Map(); const placeholderRegistry = new Map();
[...invokedPrograms.entries()].forEach(([programId, transactionCount]) => { [...invokedPrograms.entries()].forEach(([programId, transactionCount]) => {

View File

@ -2,7 +2,7 @@ import React from "react";
import { SignatureProps } from "pages/TransactionDetailsPage"; import { SignatureProps } from "pages/TransactionDetailsPage";
import { useTransactionDetails } from "providers/transactions"; import { useTransactionDetails } from "providers/transactions";
import { ProgramLogsCardBody } from "components/ProgramLogsCardBody"; import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
import { prettyProgramLogs } from "utils/program-logs"; import { parseProgramLogs } from "utils/program-logs";
import { useCluster } from "providers/cluster"; import { useCluster } from "providers/cluster";
export function ProgramLogSection({ signature }: SignatureProps) { export function ProgramLogSection({ signature }: SignatureProps) {
@ -18,7 +18,7 @@ export function ProgramLogSection({ signature }: SignatureProps) {
let prettyLogs = null; let prettyLogs = null;
if (logMessages !== null) { if (logMessages !== null) {
prettyLogs = prettyProgramLogs(logMessages, err, cluster); prettyLogs = parseProgramLogs(logMessages, err, cluster);
} }
return ( return (

View File

@ -2,7 +2,7 @@ import React from "react";
import bs58 from "bs58"; import bs58 from "bs58";
import { Connection, Message, Transaction } from "@solana/web3.js"; import { Connection, Message, Transaction } from "@solana/web3.js";
import { useCluster } from "providers/cluster"; import { useCluster } from "providers/cluster";
import { InstructionLogs, prettyProgramLogs } from "utils/program-logs"; import { InstructionLogs, parseProgramLogs } from "utils/program-logs";
import { ProgramLogsCardBody } from "components/ProgramLogsCardBody"; import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0)); const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
@ -111,7 +111,7 @@ function useSimulator(message: Message) {
} }
// Prettify logs // Prettify logs
setLogs(prettyProgramLogs(resp.value.logs, resp.value.err, cluster)); setLogs(parseProgramLogs(resp.value.logs, resp.value.err, cluster));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setLogs(null); setLogs(null);

View File

@ -12,10 +12,12 @@ export type LogMessage = {
export type InstructionLogs = { export type InstructionLogs = {
invokedProgram: string | null; invokedProgram: string | null;
logs: LogMessage[]; logs: LogMessage[];
computeUnits: number;
truncated: boolean;
failed: boolean; failed: boolean;
}; };
export function prettyProgramLogs( export function parseProgramLogs(
logs: string[], logs: string[],
error: TransactionError | null, error: TransactionError | null,
cluster: Cluster cluster: Cluster
@ -44,6 +46,8 @@ export function prettyProgramLogs(
text: log, text: log,
style: "muted", style: "muted",
}); });
} else if (log.startsWith("Log truncated")) {
prettyLogs[prettyLogs.length - 1].truncated = true;
} else { } else {
const regex = /Program (\w*) invoke \[(\d)\]/g; const regex = /Program (\w*) invoke \[(\d)\]/g;
const matches = [...log.matchAll(regex)]; const matches = [...log.matchAll(regex)];
@ -56,7 +60,9 @@ export function prettyProgramLogs(
prettyLogs.push({ prettyLogs.push({
invokedProgram: programAddress, invokedProgram: programAddress,
logs: [], logs: [],
computeUnits: 0,
failed: false, failed: false,
truncated: false,
}); });
} else { } else {
prettyLogs[prettyLogs.length - 1].logs.push({ prettyLogs[prettyLogs.length - 1].logs.push({
@ -88,15 +94,27 @@ export function prettyProgramLogs(
prettyLogs.push({ prettyLogs.push({
invokedProgram: null, invokedProgram: null,
logs: [], logs: [],
computeUnits: 0,
failed: false, failed: false,
truncated: false,
}); });
depth++; depth++;
} }
// Remove redundant program address from logs // Remove redundant program address from logs
log = log.replace(/Program \w* consumed (.*)/g, (match, p1) => { log = log.replace(
return `Program consumed: ${p1}`; /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:" // native program logs don't start with "Program log:"
prettyLogs[prettyLogs.length - 1].logs.push({ prettyLogs[prettyLogs.length - 1].logs.push({
@ -114,7 +132,9 @@ export function prettyProgramLogs(
prettyLogs.push({ prettyLogs.push({
invokedProgram: null, invokedProgram: null,
logs: [], logs: [],
computeUnits: 0,
failed: true, failed: true,
truncated: false,
}); });
} }