Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

explorer: Display invoked programs in block transaction list #19815

Merged
merged 1 commit into from
Sep 13, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
explorer: Display invoked programs in block transaction list
  • Loading branch information
jstarry committed Sep 12, 2021
commit aafc0d97ee78d77e60568386ce862720b971b594
225 changes: 217 additions & 8 deletions explorer/src/components/block/BlockHistoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,176 @@
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();

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);
}

if (block.transactions.length === 0) {
return <ErrorCard text="This block has no transactions" />;
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 {
statusClass = "success";
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}
</span>
</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>
);
})}
Expand All @@ -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>
);
};