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:
parent
b730d1da00
commit
1d6df736a3
|
@ -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]) => {
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue