explorer: Display invoked programs in block transaction list (#19815)
This commit is contained in:
parent
3bf7c3d53e
commit
af005d79ad
|
@ -1,37 +1,138 @@
|
||||||
import React from "react";
|
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 { ErrorCard } from "components/common/ErrorCard";
|
||||||
import { Signature } from "components/common/Signature";
|
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 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 }) {
|
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 filter = useQueryFilter();
|
||||||
|
|
||||||
if (block.transactions.length === 0) {
|
const { transactions, invokedPrograms } = React.useMemo(() => {
|
||||||
return <ErrorCard text="This block has no transactions" />;
|
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 (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header align-items-center">
|
<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>
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
<div className="table-responsive mb-0">
|
||||||
<table className="table table-sm table-nowrap card-table">
|
<table className="table table-sm table-nowrap card-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<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">Invoked Programs</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="list">
|
<tbody className="list">
|
||||||
{block.transactions.slice(0, numDisplayed).map((tx, i) => {
|
{filteredTransactions.slice(0, numDisplayed).map((tx, i) => {
|
||||||
let statusText;
|
let statusText;
|
||||||
let statusClass;
|
let statusClass;
|
||||||
let signature: React.ReactNode;
|
let signature: React.ReactNode;
|
||||||
if (tx.meta?.err || tx.transaction.signatures.length === 0) {
|
if (tx.meta?.err || !tx.signature) {
|
||||||
statusClass = "warning";
|
statusClass = "warning";
|
||||||
statusText = "Failed";
|
statusText = "Failed";
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,14 +140,18 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
|
||||||
statusText = "Success";
|
statusText = "Success";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tx.transaction.signatures.length > 0) {
|
if (tx.signature) {
|
||||||
signature = (
|
signature = (
|
||||||
<Signature signature={tx.transaction.signatures[0]} link />
|
<Signature signature={tx.signature} link truncateChars={48} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entries = [...tx.invocations.entries()];
|
||||||
|
entries.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
|
<td>{tx.index + 1}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`badge badge-soft-${statusClass}`}>
|
<span className={`badge badge-soft-${statusClass}`}>
|
||||||
{statusText}
|
{statusText}
|
||||||
|
@ -54,6 +159,18 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{signature}</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>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -76,3 +193,95 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue