Explorer: Refactor parsed transaction handling (#27050)

This commit is contained in:
Justin Starry 2022-08-10 12:04:54 +01:00 committed by GitHub
parent c03f3b1436
commit 9348a23dfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 99 additions and 90 deletions

View File

@ -394,8 +394,8 @@ const TokenTransactionRow = React.memo(
statusText = "Success"; statusText = "Success";
} }
const instructions = const transactionWithMeta = details?.data?.transactionWithMeta;
details?.data?.transaction?.transaction.message.instructions; const instructions = transactionWithMeta?.transaction.message.instructions;
if (!instructions) if (!instructions)
return ( return (
<tr key={tx.signature}> <tr key={tx.signature}>
@ -424,9 +424,7 @@ const TokenTransactionRow = React.memo(
let tokenInstructionNames: InstructionType[] = []; let tokenInstructionNames: InstructionType[] = [];
if (details?.data?.transaction) { if (transactionWithMeta) {
const transaction = details.data.transaction;
tokenInstructionNames = instructions tokenInstructionNames = instructions
.map((ix, index): InstructionType | undefined => { .map((ix, index): InstructionType | undefined => {
let name = "Unknown"; let name = "Unknown";
@ -437,11 +435,11 @@ const TokenTransactionRow = React.memo(
)[] = []; )[] = [];
if ( if (
transaction.meta?.innerInstructions && transactionWithMeta.meta?.innerInstructions &&
(cluster !== Cluster.MainnetBeta || (cluster !== Cluster.MainnetBeta ||
transaction.slot >= INNER_INSTRUCTIONS_START_SLOT) transactionWithMeta.slot >= INNER_INSTRUCTIONS_START_SLOT)
) { ) {
transaction.meta.innerInstructions.forEach((ix) => { transactionWithMeta.meta.innerInstructions.forEach((ix) => {
if (ix.index === index) { if (ix.index === index) {
ix.instructions.forEach((inner) => { ix.instructions.forEach((inner) => {
innerInstructions.push(inner); innerInstructions.push(inner);
@ -451,9 +449,9 @@ const TokenTransactionRow = React.memo(
} }
let transactionInstruction; let transactionInstruction;
if (transaction?.transaction) { if (transactionWithMeta?.transaction) {
transactionInstruction = intoTransactionInstruction( transactionInstruction = intoTransactionInstruction(
transaction.transaction, transactionWithMeta.transaction,
ix ix
); );
} }

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { import {
ParsedConfirmedTransaction, ParsedTransactionWithMeta,
ParsedInstruction, ParsedInstruction,
PartiallyDecodedInstruction, PartiallyDecodedInstruction,
PublicKey, PublicKey,
@ -47,23 +47,23 @@ export function TokenInstructionsCard({ pubkey }: { pubkey: PublicKey }) {
const { hasTimestamps, detailsList } = React.useMemo(() => { const { hasTimestamps, detailsList } = React.useMemo(() => {
const detailedHistoryMap = const detailedHistoryMap =
history?.data?.transactionMap || history?.data?.transactionMap ||
new Map<string, ParsedConfirmedTransaction>(); new Map<string, ParsedTransactionWithMeta>();
const hasTimestamps = transactionRows.some((element) => element.blockTime); const hasTimestamps = transactionRows.some((element) => element.blockTime);
const detailsList: React.ReactNode[] = []; const detailsList: React.ReactNode[] = [];
const mintMap = new Map<string, MintDetails>(); const mintMap = new Map<string, MintDetails>();
transactionRows.forEach( transactionRows.forEach(
({ signatureInfo, signature, blockTime, statusClass, statusText }) => { ({ signatureInfo, signature, blockTime, statusClass, statusText }) => {
const parsed = detailedHistoryMap.get(signature); const transactionWithMeta = detailedHistoryMap.get(signature);
if (!parsed) return; if (!transactionWithMeta) return;
extractMintDetails(parsed, mintMap); extractMintDetails(transactionWithMeta, mintMap);
let instructions: (ParsedInstruction | PartiallyDecodedInstruction)[] = let instructions: (ParsedInstruction | PartiallyDecodedInstruction)[] =
[]; [];
InstructionContainer.create(parsed).instructions.forEach( InstructionContainer.create(transactionWithMeta).instructions.forEach(
({ instruction, inner }, index) => { ({ instruction, inner }) => {
if (isRelevantInstruction(pubkey, address, mintMap, instruction)) { if (isRelevantInstruction(pubkey, address, mintMap, instruction)) {
instructions.push(instruction); instructions.push(instruction);
} }
@ -79,7 +79,7 @@ export function TokenInstructionsCard({ pubkey }: { pubkey: PublicKey }) {
const programId = ix.programId; const programId = ix.programId;
const instructionName = getTokenInstructionName( const instructionName = getTokenInstructionName(
parsed, transactionWithMeta,
ix, ix,
signatureInfo signatureInfo
); );

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { import {
ParsedConfirmedTransaction, ParsedTransactionWithMeta,
ParsedInstruction, ParsedInstruction,
PartiallyDecodedInstruction, PartiallyDecodedInstruction,
PublicKey, PublicKey,
@ -68,23 +68,23 @@ export function TokenTransfersCard({ pubkey }: { pubkey: PublicKey }) {
const { hasTimestamps, detailsList } = React.useMemo(() => { const { hasTimestamps, detailsList } = React.useMemo(() => {
const detailedHistoryMap = const detailedHistoryMap =
history?.data?.transactionMap || history?.data?.transactionMap ||
new Map<string, ParsedConfirmedTransaction>(); new Map<string, ParsedTransactionWithMeta>();
const hasTimestamps = transactionRows.some((element) => element.blockTime); const hasTimestamps = transactionRows.some((element) => element.blockTime);
const detailsList: React.ReactNode[] = []; const detailsList: React.ReactNode[] = [];
const mintMap = new Map<string, MintDetails>(); const mintMap = new Map<string, MintDetails>();
transactionRows.forEach( transactionRows.forEach(
({ signature, blockTime, statusText, statusClass }) => { ({ signature, blockTime, statusText, statusClass }) => {
const parsed = detailedHistoryMap.get(signature); const transactionWithMeta = detailedHistoryMap.get(signature);
if (!parsed) return; if (!transactionWithMeta) return;
// Extract mint information from token deltas // Extract mint information from token deltas
// (used to filter out non-checked tokens transfers not belonging to this mint) // (used to filter out non-checked tokens transfers not belonging to this mint)
extractMintDetails(parsed, mintMap); extractMintDetails(transactionWithMeta, mintMap);
// Extract all transfers from transaction // Extract all transfers from transaction
let transfers: IndexedTransfer[] = []; let transfers: IndexedTransfer[] = [];
InstructionContainer.create(parsed).instructions.forEach( InstructionContainer.create(transactionWithMeta).instructions.forEach(
({ instruction, inner }, index) => { ({ instruction, inner }, index) => {
const transfer = getTransfer(instruction, cluster, signature); const transfer = getTransfer(instruction, cluster, signature);
if (transfer) { if (transfer) {

View File

@ -1,4 +1,4 @@
import { ParsedConfirmedTransaction } from "@solana/web3.js"; import { ParsedTransactionWithMeta } from "@solana/web3.js";
export type MintDetails = { export type MintDetails = {
decimals: number; decimals: number;
@ -6,13 +6,15 @@ export type MintDetails = {
}; };
export function extractMintDetails( export function extractMintDetails(
parsedTransaction: ParsedConfirmedTransaction, transactionWithMeta: ParsedTransactionWithMeta,
mintMap: Map<string, MintDetails> mintMap: Map<string, MintDetails>
) { ) {
if (parsedTransaction.meta?.preTokenBalances) { if (transactionWithMeta.meta?.preTokenBalances) {
parsedTransaction.meta.preTokenBalances.forEach((balance) => { transactionWithMeta.meta.preTokenBalances.forEach((balance) => {
const account = const account =
parsedTransaction.transaction.message.accountKeys[balance.accountIndex]; transactionWithMeta.transaction.message.accountKeys[
balance.accountIndex
];
mintMap.set(account.pubkey.toBase58(), { mintMap.set(account.pubkey.toBase58(), {
decimals: balance.uiTokenAmount.decimals, decimals: balance.uiTokenAmount.decimals,
mint: balance.mint, mint: balance.mint,
@ -20,10 +22,12 @@ export function extractMintDetails(
}); });
} }
if (parsedTransaction.meta?.postTokenBalances) { if (transactionWithMeta.meta?.postTokenBalances) {
parsedTransaction.meta.postTokenBalances.forEach((balance) => { transactionWithMeta.meta.postTokenBalances.forEach((balance) => {
const account = const account =
parsedTransaction.transaction.message.accountKeys[balance.accountIndex]; transactionWithMeta.transaction.message.accountKeys[
balance.accountIndex
];
mintMap.set(account.pubkey.toBase58(), { mintMap.set(account.pubkey.toBase58(), {
decimals: balance.uiTokenAmount.decimals, decimals: balance.uiTokenAmount.decimals,
mint: balance.mint, mint: balance.mint,

View File

@ -66,11 +66,11 @@ export function InstructionsSection({ signature }: SignatureProps) {
const refreshDetails = () => fetchDetails(signature); const refreshDetails = () => fetchDetails(signature);
const result = status?.data?.info?.result; const result = status?.data?.info?.result;
if (!result || !details?.data?.transaction) { const transactionWithMeta = details?.data?.transactionWithMeta;
if (!result || !transactionWithMeta) {
return <ErrorCard retry={refreshDetails} text="No instructions found" />; return <ErrorCard retry={refreshDetails} text="No instructions found" />;
} }
const { meta } = details.data.transaction; const { meta, transaction } = transactionWithMeta;
const { transaction } = details.data?.transaction;
if (transaction.message.instructions.length === 0) { if (transaction.message.instructions.length === 0) {
return <ErrorCard retry={refreshDetails} text="No instructions found" />; return <ErrorCard retry={refreshDetails} text="No instructions found" />;
@ -83,7 +83,7 @@ export function InstructionsSection({ signature }: SignatureProps) {
if ( if (
meta?.innerInstructions && meta?.innerInstructions &&
(cluster !== Cluster.MainnetBeta || (cluster !== Cluster.MainnetBeta ||
details.data.transaction.slot >= INNER_INSTRUCTIONS_START_SLOT) transactionWithMeta.slot >= INNER_INSTRUCTIONS_START_SLOT)
) { ) {
meta.innerInstructions.forEach((parsed: ParsedInnerInstruction) => { meta.innerInstructions.forEach((parsed: ParsedInnerInstruction) => {
if (!innerInstructions[parsed.index]) { if (!innerInstructions[parsed.index]) {

View File

@ -9,12 +9,12 @@ export function ProgramLogSection({ signature }: SignatureProps) {
const { cluster, url } = useCluster(); const { cluster, url } = useCluster();
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const transaction = details?.data?.transaction; const transactionWithMeta = details?.data?.transactionWithMeta;
if (!transaction) return null; if (!transactionWithMeta) return null;
const message = transaction.transaction.message; const message = transactionWithMeta.transaction.message;
const logMessages = transaction.meta?.logMessages || null; const logMessages = transactionWithMeta.meta?.logMessages || null;
const err = transaction.meta?.err || null; const err = transactionWithMeta.meta?.err || null;
let prettyLogs = null; let prettyLogs = null;
if (logMessages !== null) { if (logMessages !== null) {

View File

@ -28,11 +28,10 @@ export function TokenBalancesCard({ signature }: SignatureProps) {
return null; return null;
} }
const preTokenBalances = details.data?.transaction?.meta?.preTokenBalances; const transactionWithMeta = details.data?.transactionWithMeta;
const postTokenBalances = details.data?.transaction?.meta?.postTokenBalances; const preTokenBalances = transactionWithMeta?.meta?.preTokenBalances;
const postTokenBalances = transactionWithMeta?.meta?.postTokenBalances;
const accountKeys = const accountKeys = transactionWithMeta?.transaction.message.accountKeys;
details.data?.transaction?.transaction.message.accountKeys;
if (!preTokenBalances || !postTokenBalances || !accountKeys) { if (!preTokenBalances || !postTokenBalances || !accountKeys) {
return null; return null;

View File

@ -193,8 +193,9 @@ function StatusCard({
} }
} }
const fee = details?.data?.transaction?.meta?.fee; const transactionWithMeta = details?.data?.transactionWithMeta;
const transaction = details?.data?.transaction?.transaction; const fee = transactionWithMeta?.meta?.fee;
const transaction = transactionWithMeta?.transaction;
const blockhash = transaction?.message.recentBlockhash; const blockhash = transaction?.message.recentBlockhash;
const isNonce = (() => { const isNonce = (() => {
if (!transaction || transaction.message.instructions.length < 1) { if (!transaction || transaction.message.instructions.length < 1) {
@ -338,7 +339,8 @@ function DetailsSection({ signature }: SignatureProps) {
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const fetchDetails = useFetchTransactionDetails(); const fetchDetails = useFetchTransactionDetails();
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
const transaction = details?.data?.transaction?.transaction; const transactionWithMeta = details?.data?.transactionWithMeta;
const transaction = transactionWithMeta?.transaction;
const message = transaction?.message; const message = transaction?.message;
const { status: clusterStatus } = useCluster(); const { status: clusterStatus } = useCluster();
const refreshDetails = () => fetchDetails(signature); const refreshDetails = () => fetchDetails(signature);
@ -360,7 +362,7 @@ function DetailsSection({ signature }: SignatureProps) {
return <LoadingCard />; return <LoadingCard />;
} else if (details.status === FetchStatus.FetchFailed) { } else if (details.status === FetchStatus.FetchFailed) {
return <ErrorCard retry={refreshDetails} text="Failed to fetch details" />; return <ErrorCard retry={refreshDetails} text="Failed to fetch details" />;
} else if (!details.data?.transaction || !message) { } else if (!transactionWithMeta || !message) {
return <ErrorCard text="Details are not available" />; return <ErrorCard text="Details are not available" />;
} }
@ -377,11 +379,12 @@ function DetailsSection({ signature }: SignatureProps) {
function AccountsCard({ signature }: SignatureProps) { function AccountsCard({ signature }: SignatureProps) {
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
if (!details?.data?.transaction) { const transactionWithMeta = details?.data?.transactionWithMeta;
if (!transactionWithMeta) {
return null; return null;
} }
const { meta, transaction } = details.data.transaction; const { meta, transaction } = transactionWithMeta;
const { message } = transaction; const { message } = transaction;
if (!meta) { if (!meta) {

View File

@ -4,7 +4,7 @@ import {
ConfirmedSignatureInfo, ConfirmedSignatureInfo,
TransactionSignature, TransactionSignature,
Connection, Connection,
ParsedConfirmedTransaction, ParsedTransactionWithMeta,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster"; import { useCluster, Cluster } from "../cluster";
import * as Cache from "providers/cache"; import * as Cache from "providers/cache";
@ -13,7 +13,7 @@ import { reportError } from "utils/sentry";
const MAX_TRANSACTION_BATCH_SIZE = 10; const MAX_TRANSACTION_BATCH_SIZE = 10;
type TransactionMap = Map<string, ParsedConfirmedTransaction>; type TransactionMap = Map<string, ParsedTransactionWithMeta>;
type AccountHistory = { type AccountHistory = {
fetched: ConfirmedSignatureInfo[]; fetched: ConfirmedSignatureInfo[];
@ -109,11 +109,14 @@ async function fetchParsedTransactions(
0, 0,
MAX_TRANSACTION_BATCH_SIZE MAX_TRANSACTION_BATCH_SIZE
); );
const fetched = await connection.getParsedConfirmedTransactions(signatures); const fetched = await connection.getParsedTransactions(signatures);
fetched.forEach( fetched.forEach(
(parsed: ParsedConfirmedTransaction | null, index: number) => { (
if (parsed !== null) { transactionWithMeta: ParsedTransactionWithMeta | null,
transactionMap.set(signatures[index], parsed); index: number
) => {
if (transactionWithMeta !== null) {
transactionMap.set(signatures[index], transactionWithMeta);
} }
} }
); );

View File

@ -2,7 +2,7 @@ import React from "react";
import { import {
Connection, Connection,
TransactionSignature, TransactionSignature,
ParsedConfirmedTransaction, ParsedTransactionWithMeta,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster"; import { useCluster, Cluster } from "../cluster";
import * as Cache from "providers/cache"; import * as Cache from "providers/cache";
@ -10,7 +10,7 @@ import { ActionType, FetchStatus } from "providers/cache";
import { reportError } from "utils/sentry"; import { reportError } from "utils/sentry";
export interface Details { export interface Details {
transaction?: ParsedConfirmedTransaction | null; transactionWithMeta?: ParsedTransactionWithMeta | null;
} }
type State = Cache.State<Details>; type State = Cache.State<Details>;
@ -53,9 +53,9 @@ async function fetchDetails(
}); });
let fetchStatus; let fetchStatus;
let transaction; let transactionWithMeta;
try { try {
transaction = await new Connection(url).getParsedConfirmedTransaction( transactionWithMeta = await new Connection(url).getParsedTransaction(
signature, signature,
"confirmed" "confirmed"
); );
@ -70,7 +70,7 @@ async function fetchDetails(
type: ActionType.Update, type: ActionType.Update,
status: fetchStatus, status: fetchStatus,
key: signature, key: signature,
data: { transaction }, data: { transactionWithMeta },
url, url,
}); });
} }

View File

@ -7,7 +7,7 @@ import { ParsedInfo } from "validators";
import { reportError } from "utils/sentry"; import { reportError } from "utils/sentry";
import { import {
ConfirmedSignatureInfo, ConfirmedSignatureInfo,
ParsedConfirmedTransaction, ParsedTransactionWithMeta,
ParsedInstruction, ParsedInstruction,
PartiallyDecodedInstruction, PartiallyDecodedInstruction,
} from "@solana/web3.js"; } from "@solana/web3.js";
@ -43,30 +43,31 @@ export interface InstructionItem {
export class InstructionContainer { export class InstructionContainer {
readonly instructions: InstructionItem[]; readonly instructions: InstructionItem[];
static create(parsedTransaction: ParsedConfirmedTransaction) { static create(transactionWithMeta: ParsedTransactionWithMeta) {
return new InstructionContainer(parsedTransaction); return new InstructionContainer(transactionWithMeta);
} }
constructor(parsedTransaction: ParsedConfirmedTransaction) { constructor(transactionWithMeta: ParsedTransactionWithMeta) {
this.instructions = parsedTransaction.transaction.message.instructions.map( this.instructions =
(instruction) => { transactionWithMeta.transaction.message.instructions.map(
if ("parsed" in instruction) { (instruction) => {
if (typeof instruction.parsed === "object") { if ("parsed" in instruction) {
instruction.parsed = create(instruction.parsed, ParsedInfo); if (typeof instruction.parsed === "object") {
} else if (typeof instruction.parsed !== "string") { instruction.parsed = create(instruction.parsed, ParsedInfo);
throw new Error("Unexpected parsed response"); } else if (typeof instruction.parsed !== "string") {
throw new Error("Unexpected parsed response");
}
} }
return {
instruction,
inner: [],
};
} }
);
return { if (transactionWithMeta.meta?.innerInstructions) {
instruction, for (let inner of transactionWithMeta.meta.innerInstructions) {
inner: [],
};
}
);
if (parsedTransaction.meta?.innerInstructions) {
for (let inner of parsedTransaction.meta.innerInstructions) {
this.instructions[inner.index].inner.push(...inner.instructions); this.instructions[inner.index].inner.push(...inner.instructions);
} }
} }
@ -89,16 +90,16 @@ export function getTokenProgramInstructionName(
} }
export function getTokenInstructionName( export function getTokenInstructionName(
transaction: ParsedConfirmedTransaction, transactionWithMeta: ParsedTransactionWithMeta,
ix: ParsedInstruction | PartiallyDecodedInstruction, ix: ParsedInstruction | PartiallyDecodedInstruction,
signatureInfo: ConfirmedSignatureInfo signatureInfo: ConfirmedSignatureInfo
) { ) {
let name = "Unknown"; let name = "Unknown";
let transactionInstruction; let transactionInstruction;
if (transaction?.transaction) { if (transactionWithMeta?.transaction) {
transactionInstruction = intoTransactionInstruction( transactionInstruction = intoTransactionInstruction(
transaction.transaction, transactionWithMeta.transaction,
ix ix
); );
} }
@ -163,7 +164,7 @@ export function getTokenInstructionName(
} }
export function getTokenInstructionType( export function getTokenInstructionType(
transaction: ParsedConfirmedTransaction, transactionWithMeta: ParsedTransactionWithMeta,
ix: ParsedInstruction | PartiallyDecodedInstruction, ix: ParsedInstruction | PartiallyDecodedInstruction,
signatureInfo: ConfirmedSignatureInfo, signatureInfo: ConfirmedSignatureInfo,
index: number index: number
@ -171,8 +172,8 @@ export function getTokenInstructionType(
const innerInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[] = const innerInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[] =
[]; [];
if (transaction.meta?.innerInstructions) { if (transactionWithMeta.meta?.innerInstructions) {
transaction.meta.innerInstructions.forEach((ix) => { transactionWithMeta.meta.innerInstructions.forEach((ix) => {
if (ix.index === index) { if (ix.index === index) {
ix.instructions.forEach((inner) => { ix.instructions.forEach((inner) => {
innerInstructions.push(inner); innerInstructions.push(inner);
@ -182,7 +183,8 @@ export function getTokenInstructionType(
} }
let name = let name =
getTokenInstructionName(transaction, ix, signatureInfo) || "Unknown"; getTokenInstructionName(transactionWithMeta, ix, signatureInfo) ||
"Unknown";
return { return {
name, name,