From 2b11ac8afe92d319d88ff0984552ea899dc352f3 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Thu, 15 Sep 2022 15:33:33 -0400 Subject: [PATCH] Explorer: Support displaying and inspecting versioned transactions --- .../src/components/ProgramLogsCardBody.tsx | 27 ++- .../components/block/BlockAccountsCard.tsx | 13 +- .../src/components/block/BlockHistoryCard.tsx | 22 ++- .../components/block/BlockOverviewCard.tsx | 4 +- .../components/block/BlockProgramsCard.tsx | 21 ++- .../src/components/block/BlockRewardsCard.tsx | 4 +- explorer/src/pages/TransactionDetailsPage.tsx | 21 ++- explorer/src/pages/inspector/AccountsCard.tsx | 154 ++++++++++++++---- .../inspector/AddressTableLookupsCard.tsx | 122 ++++++++++++++ .../pages/inspector/AddressWithContext.tsx | 38 +++++ .../src/pages/inspector/InspectorPage.tsx | 14 +- .../pages/inspector/InstructionsSection.tsx | 63 +++++-- explorer/src/pages/inspector/RawInputCard.tsx | 13 +- .../src/pages/inspector/SignaturesCard.tsx | 6 +- .../src/pages/inspector/SimulatorCard.tsx | 24 ++- explorer/src/providers/accounts/history.tsx | 4 +- explorer/src/providers/accounts/index.tsx | 62 ++++++- explorer/src/providers/block.tsx | 8 +- .../src/providers/transactions/parsed.tsx | 2 +- explorer/src/providers/transactions/raw.tsx | 18 +- .../accounts/address-lookup-table.ts | 32 ++++ 21 files changed, 538 insertions(+), 134 deletions(-) create mode 100644 explorer/src/pages/inspector/AddressTableLookupsCard.tsx create mode 100644 explorer/src/validators/accounts/address-lookup-table.ts diff --git a/explorer/src/components/ProgramLogsCardBody.tsx b/explorer/src/components/ProgramLogsCardBody.tsx index fff28b8b8ce112..b1b1cbd445acca 100644 --- a/explorer/src/components/ProgramLogsCardBody.tsx +++ b/explorer/src/components/ProgramLogsCardBody.tsx @@ -1,4 +1,4 @@ -import { Message, ParsedMessage } from "@solana/web3.js"; +import { ParsedMessage, PublicKey, VersionedMessage } from "@solana/web3.js"; import { Cluster } from "providers/cluster"; import { TableCardBody } from "components/common/TableCardBody"; import { InstructionLogs } from "utils/program-logs"; @@ -21,27 +21,24 @@ export function ProgramLogsCardBody({ cluster, url, }: { - message: Message | ParsedMessage; + message: VersionedMessage | ParsedMessage; logs: InstructionLogs[]; cluster: Cluster; url: string; }) { let logIndex = 0; + let instructionProgramIds: PublicKey[]; + if ("compiledInstructions" in message) { + instructionProgramIds = message.compiledInstructions.map((ix) => { + return message.staticAccountKeys[ix.programIdIndex]; + }); + } else { + instructionProgramIds = message.instructions.map((ix) => ix.programId); + } + return ( - {message.instructions.map((ix, index) => { - let programId; - if ("programIdIndex" in ix) { - const programAccount = message.accountKeys[ix.programIdIndex]; - if ("pubkey" in programAccount) { - programId = programAccount.pubkey; - } else { - programId = programAccount; - } - } else { - programId = ix.programId; - } - + {instructionProgramIds.map((programId, index) => { const programAddress = programId.toBase58(); let programLogs: InstructionLogs | undefined = logs[logIndex]; if (programLogs?.invokedProgram === programAddress) { diff --git a/explorer/src/components/block/BlockAccountsCard.tsx b/explorer/src/components/block/BlockAccountsCard.tsx index 2d1c0a7ab635a8..98c77f6691a763 100644 --- a/explorer/src/components/block/BlockAccountsCard.tsx +++ b/explorer/src/components/block/BlockAccountsCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { BlockResponse, PublicKey } from "@solana/web3.js"; +import { PublicKey, VersionedBlockResponse } from "@solana/web3.js"; import { Address } from "components/common/Address"; import { Link } from "react-router-dom"; import { clusterPath } from "utils/url"; @@ -15,7 +15,7 @@ export function BlockAccountsCard({ block, blockSlot, }: { - block: BlockResponse; + block: VersionedBlockResponse; blockSlot: number; }) { const [numDisplayed, setNumDisplayed] = React.useState(10); @@ -26,9 +26,12 @@ export function BlockAccountsCard({ block.transactions.forEach((tx) => { const message = tx.transaction.message; const txSet = new Map(); - message.instructions.forEach((ix) => { - ix.accounts.forEach((index) => { - const address = message.accountKeys[index].toBase58(); + const accountKeys = message.getAccountKeys({ + accountKeysFromLookups: tx.meta?.loadedAddresses, + }); + message.compiledInstructions.forEach((ix) => { + ix.accountKeyIndexes.forEach((index) => { + const address = accountKeys.get(index)!.toBase58(); txSet.set(address, message.isAccountWritable(index)); }); }); diff --git a/explorer/src/components/block/BlockHistoryCard.tsx b/explorer/src/components/block/BlockHistoryCard.tsx index e516e960afa444..876b84d596500e 100644 --- a/explorer/src/components/block/BlockHistoryCard.tsx +++ b/explorer/src/components/block/BlockHistoryCard.tsx @@ -2,11 +2,11 @@ import React from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; import { Location } from "history"; import { - BlockResponse, ConfirmedTransactionMeta, TransactionSignature, PublicKey, VOTE_PROGRAM_ID, + VersionedBlockResponse, } from "@solana/web3.js"; import { ErrorCard } from "components/common/ErrorCard"; import { Signature } from "components/common/Signature"; @@ -51,7 +51,7 @@ type TransactionWithInvocations = { logTruncated: boolean; }; -export function BlockHistoryCard({ block }: { block: BlockResponse }) { +export function BlockHistoryCard({ block }: { block: VersionedBlockResponse }) { const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE); const [showDropdown, setDropdown] = React.useState(false); const query = useQuery(); @@ -72,7 +72,7 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { signature = tx.transaction.signatures[0]; } - let programIndexes = tx.transaction.message.instructions + let programIndexes = tx.transaction.message.compiledInstructions .map((ix) => ix.programIdIndex) .concat( tx.meta?.innerInstructions?.flatMap((ix) => { @@ -87,8 +87,11 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { }); const invocations = new Map(); + const accountKeys = tx.transaction.message.getAccountKeys({ + accountKeysFromLookups: tx.meta?.loadedAddresses, + }); for (const [i, count] of indexMap.entries()) { - const programId = tx.transaction.message.accountKeys[i].toBase58(); + const programId = accountKeys.get(i)!.toBase58(); invocations.set(programId, count); const programTransactionCount = invokedPrograms.get(programId) || 0; invokedPrograms.set(programId, programTransactionCount + 1); @@ -143,8 +146,15 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) { if (accountFilter === null) { return true; } - const tx = block.transactions[index].transaction; - return tx.message.accountKeys.find((key) => key.equals(accountFilter)); + + const tx = block.transactions[index]; + const accountKeys = tx.transaction.message.getAccountKeys({ + accountKeysFromLookups: tx.meta?.loadedAddresses, + }); + return accountKeys + .keySegments() + .flat() + .find((key) => key.equals(accountFilter)); }); const showComputeUnits = filteredTxs.every( diff --git a/explorer/src/components/block/BlockOverviewCard.tsx b/explorer/src/components/block/BlockOverviewCard.tsx index 15279382c72c94..1675e4c518493e 100644 --- a/explorer/src/components/block/BlockOverviewCard.tsx +++ b/explorer/src/components/block/BlockOverviewCard.tsx @@ -7,7 +7,7 @@ import { Slot } from "components/common/Slot"; import { ClusterStatus, useCluster } from "providers/cluster"; import { BlockHistoryCard } from "./BlockHistoryCard"; import { BlockRewardsCard } from "./BlockRewardsCard"; -import { BlockResponse } from "@solana/web3.js"; +import { VersionedBlockResponse } from "@solana/web3.js"; import { NavLink } from "react-router-dom"; import { clusterPath } from "utils/url"; import { BlockProgramsCard } from "./BlockProgramsCard"; @@ -211,7 +211,7 @@ function MoreSection({ tab, }: { slot: number; - block: BlockResponse; + block: VersionedBlockResponse; tab?: string; }) { return ( diff --git a/explorer/src/components/block/BlockProgramsCard.tsx b/explorer/src/components/block/BlockProgramsCard.tsx index b1a70006ab9b1c..937267f34d7658 100644 --- a/explorer/src/components/block/BlockProgramsCard.tsx +++ b/explorer/src/components/block/BlockProgramsCard.tsx @@ -1,9 +1,13 @@ import React from "react"; -import { BlockResponse, PublicKey } from "@solana/web3.js"; +import { PublicKey, VersionedBlockResponse } from "@solana/web3.js"; import { Address } from "components/common/Address"; import { TableCardBody } from "components/common/TableCardBody"; -export function BlockProgramsCard({ block }: { block: BlockResponse }) { +export function BlockProgramsCard({ + block, +}: { + block: VersionedBlockResponse; +}) { const totalTransactions = block.transactions.length; const txSuccesses = new Map(); const txFrequency = new Map(); @@ -12,18 +16,23 @@ export function BlockProgramsCard({ block }: { block: BlockResponse }) { let totalInstructions = 0; block.transactions.forEach((tx) => { const message = tx.transaction.message; - totalInstructions += message.instructions.length; + totalInstructions += message.compiledInstructions.length; const programUsed = new Set(); + const accountKeys = tx.transaction.message.getAccountKeys({ + accountKeysFromLookups: tx.meta?.loadedAddresses, + }); const trackProgram = (index: number) => { - if (index >= message.accountKeys.length) return; - const programId = message.accountKeys[index]; + if (index >= accountKeys.length) return; + const programId = accountKeys.get(index)!; const programAddress = programId.toBase58(); programUsed.add(programAddress); const frequency = ixFrequency.get(programAddress); ixFrequency.set(programAddress, frequency ? frequency + 1 : 1); }; - message.instructions.forEach((ix) => trackProgram(ix.programIdIndex)); + message.compiledInstructions.forEach((ix) => + trackProgram(ix.programIdIndex) + ); tx.meta?.innerInstructions?.forEach((inner) => { totalInstructions += inner.instructions.length; inner.instructions.forEach((innerIx) => diff --git a/explorer/src/components/block/BlockRewardsCard.tsx b/explorer/src/components/block/BlockRewardsCard.tsx index c1085b14005e04..8d4d41fc261e19 100644 --- a/explorer/src/components/block/BlockRewardsCard.tsx +++ b/explorer/src/components/block/BlockRewardsCard.tsx @@ -1,11 +1,11 @@ import React from "react"; import { SolBalance } from "utils"; -import { BlockResponse, PublicKey } from "@solana/web3.js"; +import { PublicKey, VersionedBlockResponse } from "@solana/web3.js"; import { Address } from "components/common/Address"; const PAGE_SIZE = 10; -export function BlockRewardsCard({ block }: { block: BlockResponse }) { +export function BlockRewardsCard({ block }: { block: VersionedBlockResponse }) { const [rewardsDisplayed, setRewardsDisplayed] = React.useState(PAGE_SIZE); if (!block.rewards || block.rewards.length < 1) { diff --git a/explorer/src/pages/TransactionDetailsPage.tsx b/explorer/src/pages/TransactionDetailsPage.tsx index 044c46d5028f49..2fc9d6d52ef375 100644 --- a/explorer/src/pages/TransactionDetailsPage.tsx +++ b/explorer/src/pages/TransactionDetailsPage.tsx @@ -197,6 +197,7 @@ function StatusCard({ const fee = transactionWithMeta?.meta?.fee; const transaction = transactionWithMeta?.transaction; const blockhash = transaction?.message.recentBlockhash; + const version = transactionWithMeta?.version; const isNonce = (() => { if (!transaction || transaction.message.instructions.length < 1) { return false; @@ -330,6 +331,13 @@ function StatusCard({ )} + + {version !== undefined && ( + + Transaction Version + {version} + + )} ); @@ -414,14 +422,19 @@ function AccountsCard({ signature }: SignatureProps) { {index === 0 && ( Fee Payer )} - {account.writable && ( - Writable - )} {account.signer && ( Signer )} + {account.writable && ( + Writable + )} {message.instructions.find((ix) => ix.programId.equals(pubkey)) && ( - Program + Program + )} + {account.source === "lookupTable" && ( + + Address Table Lookup + )} diff --git a/explorer/src/pages/inspector/AccountsCard.tsx b/explorer/src/pages/inspector/AccountsCard.tsx index 53702ea3fb3405..d414cd1fde7afd 100644 --- a/explorer/src/pages/inspector/AccountsCard.tsx +++ b/explorer/src/pages/inspector/AccountsCard.tsx @@ -1,10 +1,13 @@ import React from "react"; -import { Message, PublicKey } from "@solana/web3.js"; +import { PublicKey, VersionedMessage } from "@solana/web3.js"; import { TableCardBody } from "components/common/TableCardBody"; -import { AddressWithContext } from "./AddressWithContext"; +import { + AddressFromLookupTableWithContext, + AddressWithContext, +} from "./AddressWithContext"; import { ErrorCard } from "components/common/ErrorCard"; -export function AccountsCard({ message }: { message: Message }) { +export function AccountsCard({ message }: { message: VersionedMessage }) { const [expanded, setExpanded] = React.useState(true); const { validMessage, error } = React.useMemo(() => { @@ -16,9 +19,11 @@ export function AccountsCard({ message }: { message: Message }) { if (numReadonlySignedAccounts >= numRequiredSignatures) { return { validMessage: undefined, error: "Invalid header" }; - } else if (numReadonlyUnsignedAccounts >= message.accountKeys.length) { + } else if ( + numReadonlyUnsignedAccounts >= message.staticAccountKeys.length + ) { return { validMessage: undefined, error: "Invalid header" }; - } else if (message.accountKeys.length === 0) { + } else if (message.staticAccountKeys.length === 0) { return { validMessage: undefined, error: "Message has no accounts" }; } @@ -28,39 +33,86 @@ export function AccountsCard({ message }: { message: Message }) { }; }, [message]); - const accountRows = React.useMemo(() => { + const { accountRows, numAccounts } = React.useMemo(() => { const message = validMessage; - if (!message) return; - return message.accountKeys.map((publicKey, accountIndex) => { - const { - numRequiredSignatures, - numReadonlySignedAccounts, - numReadonlyUnsignedAccounts, - } = message.header; - - let readOnly = false; - let signer = false; - if (accountIndex < numRequiredSignatures) { - signer = true; - if (accountIndex >= numRequiredSignatures - numReadonlySignedAccounts) { + if (!message) return { accountRows: undefined, numAccounts: 0 }; + const staticAccountRows = message.staticAccountKeys.map( + (publicKey, accountIndex) => { + const { + numRequiredSignatures, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + } = message.header; + + let readOnly = false; + let signer = false; + if (accountIndex < numRequiredSignatures) { + signer = true; + if ( + accountIndex >= + numRequiredSignatures - numReadonlySignedAccounts + ) { + readOnly = true; + } + } else if ( + accountIndex >= + message.staticAccountKeys.length - numReadonlyUnsignedAccounts + ) { readOnly = true; } - } else if ( - accountIndex >= - message.accountKeys.length - numReadonlyUnsignedAccounts - ) { - readOnly = true; + + const props = { + accountIndex, + publicKey, + signer, + readOnly, + }; + + return ; } + ); - const props = { - accountIndex, - publicKey, - signer, - readOnly, - }; + let accountIndex = message.staticAccountKeys.length; + const writableLookupTableRows = message.addressTableLookups.flatMap( + (lookup) => { + return lookup.writableIndexes.map((lookupTableIndex) => { + const props = { + accountIndex, + lookupTableKey: lookup.accountKey, + lookupTableIndex, + readOnly: false, + }; + + accountIndex += 1; + return ; + }); + } + ); - return ; - }); + const readonlyLookupTableRows = message.addressTableLookups.flatMap( + (lookup) => { + return lookup.readonlyIndexes.map((lookupTableIndex) => { + const props = { + accountIndex, + lookupTableKey: lookup.accountKey, + lookupTableIndex, + readOnly: true, + }; + + accountIndex += 1; + return ; + }); + } + ); + + return { + accountRows: [ + ...staticAccountRows, + ...writableLookupTableRows, + ...readonlyLookupTableRows, + ], + numAccounts: accountIndex, + }; }, [validMessage]); if (error) { @@ -70,9 +122,7 @@ export function AccountsCard({ message }: { message: Message }) { return (
-

- {`Account List (${message.accountKeys.length})`} -

+

{`Account List (${numAccounts})`}

+
+ {expanded && ( +
+ + + + + + + + + + {lookupRows.length > 0 ? ( + {lookupRows} + ) : ( +
+
No entries found
+
+ )} +
Address Lookup Table AddressTable IndexResolved AddressDetails
+
+ )} +
+ ); +} + +function LookupRow({ + lookupTableKey, + lookupTableIndex, + readOnly, +}: { + lookupTableKey: PublicKey; + lookupTableIndex: number; + readOnly: boolean; +}) { + const lookupTable = useAddressLookupTable(lookupTableKey.toBase58()); + const fetchAccountInfo = useFetchAccountInfo(); + React.useEffect(() => { + if (!lookupTable) fetchAccountInfo(lookupTableKey); + }, [lookupTableKey, lookupTable, fetchAccountInfo]); + + let resolvedKeyComponent; + if (!lookupTable) { + resolvedKeyComponent = ( + + + Loading + + ); + } else if (typeof lookupTable === "string") { + resolvedKeyComponent = ( + Invalid Lookup Table + ); + } else if (lookupTableIndex < lookupTable.state.addresses.length) { + const resolvedKey = lookupTable.state.addresses[lookupTableIndex]; + resolvedKeyComponent =
; + } else { + resolvedKeyComponent = ( + Invalid Lookup Table Index + ); + } + + return ( + + +
+ + {lookupTableIndex} + {resolvedKeyComponent} + + {!readOnly && Writable} + + + ); +} diff --git a/explorer/src/pages/inspector/AddressWithContext.tsx b/explorer/src/pages/inspector/AddressWithContext.tsx index a943086066159b..057de61f693824 100644 --- a/explorer/src/pages/inspector/AddressWithContext.tsx +++ b/explorer/src/pages/inspector/AddressWithContext.tsx @@ -4,6 +4,7 @@ import { Address } from "components/common/Address"; import { Account, useAccountInfo, + useAddressLookupTable, useFetchAccountInfo, } from "providers/accounts"; import { ClusterStatus, useCluster } from "providers/cluster"; @@ -36,6 +37,43 @@ export const programValidator = (account: Account): string | undefined => { return; }; +export function AddressFromLookupTableWithContext({ + lookupTableKey, + lookupTableIndex, +}: { + lookupTableKey: PublicKey; + lookupTableIndex: number; +}) { + const lookupTable = useAddressLookupTable(lookupTableKey.toBase58()); + const fetchAccountInfo = useFetchAccountInfo(); + React.useEffect(() => { + if (!lookupTable) fetchAccountInfo(lookupTableKey); + }, [lookupTableKey, lookupTable, fetchAccountInfo]); + + let pubkey; + if (!lookupTable) { + return ( + + + Loading + + ); + } else if (typeof lookupTable === "string") { + return
Invalid Lookup Table
; + } else if (lookupTableIndex < lookupTable.state.addresses.length) { + pubkey = lookupTable.state.addresses[lookupTableIndex]; + } else { + return
Invalid Lookup Table Index
; + } + + return ( +
+
+ +
+ ); +} + export function AddressWithContext({ pubkey, validator, diff --git a/explorer/src/pages/inspector/InspectorPage.tsx b/explorer/src/pages/inspector/InspectorPage.tsx index a8ba07a7941404..7d303924b36a7d 100644 --- a/explorer/src/pages/inspector/InspectorPage.tsx +++ b/explorer/src/pages/inspector/InspectorPage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Message, PACKET_DATA_SIZE } from "@solana/web3.js"; +import { PACKET_DATA_SIZE, VersionedMessage } from "@solana/web3.js"; import { TableCardBody } from "components/common/TableCardBody"; import { SolBalance } from "utils"; @@ -14,6 +14,7 @@ import { LoadingCard } from "components/common/LoadingCard"; import { ErrorCard } from "components/common/ErrorCard"; import { TransactionSignatures } from "./SignaturesCard"; import { AccountsCard } from "./AccountsCard"; +import { AddressTableLookupsCard } from "./AddressTableLookupsCard"; import { AddressWithContext, createFeePayerValidator, @@ -25,7 +26,7 @@ import base58 from "bs58"; export type TransactionData = { rawMessage: Uint8Array; - message: Message; + message: VersionedMessage; signatures?: (string | null)[]; }; @@ -117,7 +118,7 @@ function decodeUrlParams( throw new Error("message buffer is too short"); } - const message = Message.from(buffer); + const message = VersionedMessage.deserialize(buffer); const data = { message, rawMessage: buffer, @@ -280,6 +281,7 @@ function LoadedView({ /> )} + ); @@ -294,7 +296,7 @@ function OverviewCard({ raw, onClear, }: { - message: Message; + message: VersionedMessage; raw: Uint8Array; onClear: () => void; }) { @@ -354,11 +356,11 @@ function OverviewCard({ - {message.accountKeys.length === 0 ? ( + {message.staticAccountKeys.length === 0 ? ( "No Fee Payer" ) : ( )} diff --git a/explorer/src/pages/inspector/InstructionsSection.tsx b/explorer/src/pages/inspector/InstructionsSection.tsx index baba3a012bf28f..5374bf65275b03 100644 --- a/explorer/src/pages/inspector/InstructionsSection.tsx +++ b/explorer/src/pages/inspector/InstructionsSection.tsx @@ -1,18 +1,25 @@ import React from "react"; -import bs58 from "bs58"; -import { CompiledInstruction, Message } from "@solana/web3.js"; +import { MessageCompiledInstruction, VersionedMessage } from "@solana/web3.js"; import { TableCardBody } from "components/common/TableCardBody"; -import { AddressWithContext, programValidator } from "./AddressWithContext"; +import { + AddressFromLookupTableWithContext, + AddressWithContext, + programValidator, +} from "./AddressWithContext"; import { useCluster } from "providers/cluster"; import { getProgramName } from "utils/tx"; import { HexData } from "components/common/HexData"; import getInstructionCardScrollAnchorId from "utils/get-instruction-card-scroll-anchor-id"; import { useScrollAnchor } from "providers/scroll-anchor"; -export function InstructionsSection({ message }: { message: Message }) { +export function InstructionsSection({ + message, +}: { + message: VersionedMessage; +}) { return ( <> - {message.instructions.map((ix, index) => { + {message.compiledInstructions.map((ix, index) => { return ; })} @@ -24,17 +31,31 @@ function InstructionCard({ ix, index, }: { - message: Message; - ix: CompiledInstruction; + message: VersionedMessage; + ix: MessageCompiledInstruction; index: number; }) { const [expanded, setExpanded] = React.useState(false); const { cluster } = useCluster(); - const programId = message.accountKeys[ix.programIdIndex]; + const programId = message.staticAccountKeys[ix.programIdIndex]; const programName = getProgramName(programId.toBase58(), cluster); const scrollAnchorRef = useScrollAnchor( getInstructionCardScrollAnchorId([index + 1]) ); + const lookupsForAccountKeyIndex = [ + ...message.addressTableLookups.flatMap((lookup) => + lookup.writableIndexes.map((index) => ({ + lookupTableKey: lookup.accountKey, + lookupTableIndex: index, + })) + ), + ...message.addressTableLookups.flatMap((lookup) => + lookup.readonlyIndexes.map((index) => ({ + lookupTableKey: lookup.accountKey, + lookupTableIndex: index, + })) + ), + ]; return (
@@ -58,12 +79,19 @@ function InstructionCard({ Program - {ix.accounts.map((accountIndex, index) => { + {ix.accountKeyIndexes.map((accountIndex, index) => { + let lookup; + if (accountIndex >= message.staticAccountKeys.length) { + const lookupIndex = + accountIndex - message.staticAccountKeys.length; + lookup = lookupsForAccountKeyIndex[lookupIndex]; + } + return ( @@ -82,9 +110,16 @@ function InstructionCard({
- + {lookup === undefined ? ( + + ) : ( + + )} ); @@ -94,7 +129,7 @@ function InstructionCard({ Instruction Data (Hex) - + diff --git a/explorer/src/pages/inspector/RawInputCard.tsx b/explorer/src/pages/inspector/RawInputCard.tsx index 6e725f87a2b71e..6b9f150cde8927 100644 --- a/explorer/src/pages/inspector/RawInputCard.tsx +++ b/explorer/src/pages/inspector/RawInputCard.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { Message } from "@solana/web3.js"; +import { VersionedMessage } from "@solana/web3.js"; import type { TransactionData } from "./InspectorPage"; import { useQuery } from "utils/url"; import { useHistory, useLocation } from "react-router"; import base58 from "bs58"; function deserializeTransaction(bytes: Uint8Array): { - message: Message; + message: VersionedMessage; signatures: string[]; } | null { const SIGNATURE_LENGTH = 64; @@ -19,17 +19,12 @@ function deserializeTransaction(bytes: Uint8Array): { bytes = bytes.slice(SIGNATURE_LENGTH); signatures.push(base58.encode(rawSignature)); } - - const requiredSignatures = bytes[0]; - if (requiredSignatures !== signaturesLen) { - throw new Error("Signature length mismatch"); - } } catch (err) { // Errors above indicate that the bytes do not encode a transaction. return null; } - const message = Message.from(bytes); + const message = VersionedMessage.deserialize(bytes); return { message, signatures }; } @@ -103,7 +98,7 @@ export function RawInput({ signatures: tx.signatures, }); } else { - const message = Message.from(buffer); + const message = VersionedMessage.deserialize(buffer); setTransactionData({ rawMessage: buffer, message, diff --git a/explorer/src/pages/inspector/SignaturesCard.tsx b/explorer/src/pages/inspector/SignaturesCard.tsx index 6ba22cdef6c89b..3ae61faa23c5eb 100644 --- a/explorer/src/pages/inspector/SignaturesCard.tsx +++ b/explorer/src/pages/inspector/SignaturesCard.tsx @@ -1,7 +1,7 @@ import React from "react"; import bs58 from "bs58"; import * as nacl from "tweetnacl"; -import { Message, PublicKey } from "@solana/web3.js"; +import { PublicKey, VersionedMessage } from "@solana/web3.js"; import { Signature } from "components/common/Signature"; import { Address } from "components/common/Address"; @@ -11,12 +11,12 @@ export function TransactionSignatures({ rawMessage, }: { signatures: (string | null)[]; - message: Message; + message: VersionedMessage; rawMessage: Uint8Array; }) { const signatureRows = React.useMemo(() => { return signatures.map((signature, index) => { - const publicKey = message.accountKeys[index]; + const publicKey = message.staticAccountKeys[index]; let verified; if (signature) { diff --git a/explorer/src/pages/inspector/SimulatorCard.tsx b/explorer/src/pages/inspector/SimulatorCard.tsx index 4ca2fbf66b6451..ef89e605969be0 100644 --- a/explorer/src/pages/inspector/SimulatorCard.tsx +++ b/explorer/src/pages/inspector/SimulatorCard.tsx @@ -1,13 +1,14 @@ import React from "react"; -import bs58 from "bs58"; -import { Connection, Message, Transaction } from "@solana/web3.js"; +import { + Connection, + VersionedMessage, + VersionedTransaction, +} from "@solana/web3.js"; import { useCluster } from "providers/cluster"; import { InstructionLogs, parseProgramLogs } from "utils/program-logs"; import { ProgramLogsCardBody } from "components/ProgramLogsCardBody"; -const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0)); - -export function SimulatorCard({ message }: { message: Message }) { +export function SimulatorCard({ message }: { message: VersionedMessage }) { const { cluster, url } = useCluster(); const { simulate, @@ -77,7 +78,7 @@ export function SimulatorCard({ message }: { message: Message }) { ); } -function useSimulator(message: Message) { +function useSimulator(message: VersionedMessage) { const { cluster, url } = useCluster(); const [simulating, setSimulating] = React.useState(false); const [logs, setLogs] = React.useState | null>(null); @@ -97,15 +98,10 @@ function useSimulator(message: Message) { const connection = new Connection(url, "confirmed"); (async () => { try { - const tx = Transaction.populate( - message, - new Array(message.header.numRequiredSignatures).fill( - DEFAULT_SIGNATURE - ) - ); - // Simulate without signers to skip signer verification - const resp = await connection.simulateTransaction(tx); + const resp = await connection.simulateTransaction( + new VersionedTransaction(message) + ); if (resp.value.logs === null) { throw new Error("Expected to receive logs from simulation"); } diff --git a/explorer/src/providers/accounts/history.tsx b/explorer/src/providers/accounts/history.tsx index 618a426a15e325..5df235889d53dc 100644 --- a/explorer/src/providers/accounts/history.tsx +++ b/explorer/src/providers/accounts/history.tsx @@ -109,7 +109,9 @@ async function fetchParsedTransactions( 0, MAX_TRANSACTION_BATCH_SIZE ); - const fetched = await connection.getParsedTransactions(signatures); + const fetched = await connection.getParsedTransactions(signatures, { + maxSupportedTransactionVersion: 0, + }); fetched.forEach( ( transactionWithMeta: ParsedTransactionWithMeta | null, diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx index 1618ccde7a96f1..74f5f1525729c5 100644 --- a/explorer/src/providers/accounts/index.tsx +++ b/explorer/src/providers/accounts/index.tsx @@ -1,6 +1,12 @@ import React from "react"; import { pubkeyToString } from "utils"; -import { PublicKey, Connection, StakeActivationData } from "@solana/web3.js"; +import { + PublicKey, + Connection, + StakeActivationData, + AddressLookupTableAccount, + AddressLookupTableProgram, +} from "@solana/web3.js"; import { useCluster, Cluster } from "../cluster"; import { HistoryProvider } from "./history"; import { TokensProvider } from "./tokens"; @@ -19,6 +25,7 @@ import { VoteAccount } from "validators/accounts/vote"; import { NonceAccount } from "validators/accounts/nonce"; import { SysvarAccount } from "validators/accounts/sysvar"; import { ConfigAccount } from "validators/accounts/config"; +import { ParsedAddressLookupTableAccount } from "validators/accounts/address-lookup-table"; import { FlaggedAccountsProvider } from "./flagged-accounts"; import { ProgramDataAccount, @@ -76,6 +83,11 @@ export type ConfigProgramData = { parsed: ConfigAccount; }; +export type AddressLookupTableProgramData = { + program: "address-lookup-table"; + parsed: ParsedAddressLookupTableAccount; +}; + export type ProgramData = | UpgradeableLoaderAccountData | StakeProgramData @@ -83,7 +95,8 @@ export type ProgramData = | VoteProgramData | NonceProgramData | SysvarProgramData - | ConfigProgramData; + | ConfigProgramData + | AddressLookupTableProgramData; export interface Details { executable: boolean; @@ -238,6 +251,17 @@ async function fetchAccountInfo( }; break; + case "address-lookup-table": { + const parsed = create(info, ParsedAddressLookupTableAccount); + + data = { + program: result.data.program, + parsed, + }; + + break; + } + case "spl-token": const parsed = create(info, TokenAccount); let nftData; @@ -428,6 +452,40 @@ export function useTokenAccountInfo( } } +export function useAddressLookupTable( + address: string | undefined +): AddressLookupTableAccount | undefined | string { + const accountInfo = useAccountInfo(address); + if (address === undefined) return; + if (accountInfo?.data?.details === undefined) return; + if (accountInfo.data.lamports === 0) return "Lookup Table Not Found"; + const { data, rawData } = accountInfo.data.details; + + const key = new PublicKey(address); + if (data && data.program === "address-lookup-table") { + if (data.parsed.type === "lookupTable") { + return new AddressLookupTableAccount({ + key, + state: data.parsed.info, + }); + } else if (data.parsed.type === "uninitialized") { + return "Lookup Table Uninitialized"; + } + } else if ( + rawData && + accountInfo.data.details.owner.equals(AddressLookupTableProgram.programId) + ) { + try { + return new AddressLookupTableAccount({ + key, + state: AddressLookupTableAccount.deserialize(rawData), + }); + } catch {} + } + + return "Invalid Lookup Table"; +} + export function useFetchAccountInfo() { const dispatch = React.useContext(DispatchContext); if (!dispatch) { diff --git a/explorer/src/providers/block.tsx b/explorer/src/providers/block.tsx index fa1b564950ddfc..8414413769313a 100644 --- a/explorer/src/providers/block.tsx +++ b/explorer/src/providers/block.tsx @@ -1,7 +1,7 @@ import React from "react"; import * as Sentry from "@sentry/react"; import * as Cache from "providers/cache"; -import { Connection, BlockResponse, PublicKey } from "@solana/web3.js"; +import { Connection, PublicKey, VersionedBlockResponse } from "@solana/web3.js"; import { useCluster, Cluster } from "./cluster"; export enum FetchStatus { @@ -16,7 +16,7 @@ export enum ActionType { } type Block = { - block?: BlockResponse; + block?: VersionedBlockResponse; blockLeader?: PublicKey; childSlot?: number; childLeader?: PublicKey; @@ -76,7 +76,9 @@ export async function fetchBlock( try { const connection = new Connection(url, "confirmed"); - const block = await connection.getBlock(slot); + const block = await connection.getBlock(slot, { + maxSupportedTransactionVersion: 0, + }); if (block === null) { data = {}; status = FetchStatus.Fetched; diff --git a/explorer/src/providers/transactions/parsed.tsx b/explorer/src/providers/transactions/parsed.tsx index 4ec8b2aaa03cac..1712f8d5691d77 100644 --- a/explorer/src/providers/transactions/parsed.tsx +++ b/explorer/src/providers/transactions/parsed.tsx @@ -57,7 +57,7 @@ async function fetchDetails( try { transactionWithMeta = await new Connection(url).getParsedTransaction( signature, - "confirmed" + { commitment: "confirmed", maxSupportedTransactionVersion: 0 } ); fetchStatus = FetchStatus.Fetched; } catch (error) { diff --git a/explorer/src/providers/transactions/raw.tsx b/explorer/src/providers/transactions/raw.tsx index 3e928b20e523a6..cdf29de7fe77a2 100644 --- a/explorer/src/providers/transactions/raw.tsx +++ b/explorer/src/providers/transactions/raw.tsx @@ -2,8 +2,9 @@ import React from "react"; import { Connection, TransactionSignature, - Transaction, - Message, + TransactionMessage, + DecompileArgs, + VersionedMessage, } from "@solana/web3.js"; import { useCluster, Cluster } from "../cluster"; import * as Cache from "providers/cache"; @@ -12,8 +13,8 @@ import { reportError } from "utils/sentry"; export interface Details { raw?: { - transaction: Transaction; - message: Message; + transaction: TransactionMessage; + message: VersionedMessage; signatures: string[]; } | null; } @@ -66,17 +67,22 @@ async function fetchRawTransaction( ) { let fetchStatus; try { - const response = await new Connection(url).getTransaction(signature); + const response = await new Connection(url).getTransaction(signature, { + maxSupportedTransactionVersion: 0, + }); fetchStatus = FetchStatus.Fetched; let data: Details = { raw: null }; if (response !== null) { const { message, signatures } = response.transaction; + const accountKeysFromLookups = response.meta?.loadedAddresses; + const decompileArgs: DecompileArgs | undefined = + accountKeysFromLookups && { accountKeysFromLookups }; data = { raw: { message, signatures, - transaction: Transaction.populate(message, signatures), + transaction: TransactionMessage.decompile(message, decompileArgs), }, }; } diff --git a/explorer/src/validators/accounts/address-lookup-table.ts b/explorer/src/validators/accounts/address-lookup-table.ts new file mode 100644 index 00000000000000..da8f9e46421688 --- /dev/null +++ b/explorer/src/validators/accounts/address-lookup-table.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-redeclare */ + +import { Infer, number, enums, type, array, optional } from "superstruct"; +import { PublicKeyFromString } from "validators/pubkey"; +import { BigIntFromString, NumberFromString } from "validators/number"; + +export type AddressLookupTableAccountType = Infer< + typeof AddressLookupTableAccountType +>; +export const AddressLookupTableAccountType = enums([ + "uninitialized", + "lookupTable", +]); + +export type AddressLookupTableAccountInfo = Infer< + typeof AddressLookupTableAccountInfo +>; +export const AddressLookupTableAccountInfo = type({ + deactivationSlot: BigIntFromString, + lastExtendedSlot: NumberFromString, + lastExtendedSlotStartIndex: number(), + authority: optional(PublicKeyFromString), + addresses: array(PublicKeyFromString), +}); + +export type ParsedAddressLookupTableAccount = Infer< + typeof ParsedAddressLookupTableAccount +>; +export const ParsedAddressLookupTableAccount = type({ + type: AddressLookupTableAccountType, + info: AddressLookupTableAccountInfo, +});