explorer: Display invoked programs in block transaction list (#19815)

This commit is contained in:
Justin Starry 2021-09-12 19:05:46 -05:00 committed by GitHub
parent 3bf7c3d53e
commit af005d79ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 217 additions and 8 deletions

View File

@ -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<string, number>;
};
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 <ErrorCard text="This block has no transactions" />;
const { transactions, invokedPrograms } = React.useMemo(() => {
const invokedPrograms = new Map<string, number>();
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<number, number>();
programIndexes.forEach((programIndex) => {
const count = indexMap.get(programIndex) || 0;
indexMap.set(programIndex, count + 1);
});
const invocations = new Map<string, number>();
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 <ErrorCard text={errorMessage} />;
}
let title: string;
if (filteredTransactions.length === transactions.length) {
title = `Block Transactions (${filteredTransactions.length})`;
} else {
title = `Block Transactions`;
}
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Block Transactions</h3>
<h3 className="card-header-title">{title}</h3>
<FilterDropdown
filter={filter}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
invokedPrograms={invokedPrograms}
totalTransactionCount={transactions.length}
></FilterDropdown>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">#</th>
<th className="text-muted">Result</th>
<th className="text-muted">Transaction Signature</th>
<th className="text-muted">Invoked Programs</th>
</tr>
</thead>
<tbody className="list">
{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 = (
<Signature signature={tx.transaction.signatures[0]} link />
<Signature signature={tx.signature} link truncateChars={48} />
);
}
const entries = [...tx.invocations.entries()];
entries.sort();
return (
<tr key={i}>
<td>{tx.index + 1}</td>
<td>
<span className={`badge badge-soft-${statusClass}`}>
{statusText}
@ -54,6 +159,18 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
</td>
<td>{signature}</td>
<td>
{tx.invocations.size === 0
? "NA"
: entries.map(([programId, count], i) => {
return (
<div key={i} className="d-flex align-items-center">
<Address pubkey={new PublicKey(programId)} link />
<span className="ml-2 text-muted">{`(${count})`}</span>
</div>
);
})}
</td>
</tr>
);
})}
@ -76,3 +193,95 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
</div>
);
}
type FilterProps = {
filter: string;
toggle: () => void;
show: boolean;
invokedPrograms: Map<string, number>;
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 (
<div className="dropdown mr-2">
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{currentFilterOption.name}
</button>
<div
className={`token-filter dropdown-menu-right dropdown-menu${
show ? " show" : ""
}`}
>
{filterOptions.map(({ name, programId, transactionCount }) => {
return (
<Link
key={programId}
to={(location: Location) => buildLocation(location, programId)}
className={`dropdown-item${
programId === filter ? " active" : ""
}`}
onClick={toggle}
>
{`${name} (${transactionCount})`}
</Link>
);
})}
</div>
</div>
);
};