From ce8f07e4b58c309a1e4e4ace7bb992de1d6c3a98 Mon Sep 17 00:00:00 2001
From: Justin Starry <justin@solana.com>
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    | 10 +--
 .../src/components/block/BlockHistoryCard.tsx | 28 ++++--
 .../components/block/BlockOverviewCard.tsx    |  4 +-
 .../components/block/BlockProgramsCard.tsx    | 24 ++++--
 .../src/components/block/BlockRewardsCard.tsx |  4 +-
 explorer/src/pages/TransactionDetailsPage.tsx |  8 ++
 explorer/src/pages/inspector/AccountsCard.tsx | 86 ++++++++++++++++---
 .../pages/inspector/AddressWithContext.tsx    | 31 +++++++
 .../src/pages/inspector/InspectorPage.tsx     | 12 +--
 .../pages/inspector/InstructionsSection.tsx   | 33 ++++---
 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     | 46 +++++++++-
 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 +++++++
 20 files changed, 316 insertions(+), 104 deletions(-)
 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 (
     <TableCardBody>
-      {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..823000d7bce889 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,9 @@ export function BlockAccountsCard({
     block.transactions.forEach((tx) => {
       const message = tx.transaction.message;
       const txSet = new Map<string, boolean>();
-      message.instructions.forEach((ix) => {
-        ix.accounts.forEach((index) => {
-          const address = message.accountKeys[index].toBase58();
+      message.compiledInstructions.forEach((ix) => {
+        ix.accountKeyIndexes.forEach((index) => {
+          const address = message.getAccountKeys().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..171345bac16715 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,14 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
         });
 
         const invocations = new Map<string, number>();
+        const accountKeysFromLookups = tx.meta?.loadedAddresses;
+        const accountKeys = tx.transaction.message.getAccountKeys(
+          accountKeysFromLookups && {
+            accountKeysFromLookups,
+          }
+        );
         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 +149,18 @@ 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 accountKeysFromLookups = tx.meta?.loadedAddresses;
+        const accountKeys = tx.transaction.message.getAccountKeys(
+          accountKeysFromLookups && {
+            accountKeysFromLookups,
+          }
+        );
+        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..0b9d986c47fccb 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<string, number>();
   const txFrequency = new Map<string, number>();
@@ -12,18 +16,26 @@ 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<string>();
+    const accountKeysFromLookups = tx.meta?.loadedAddresses;
+    const accountKeys = tx.transaction.message.getAccountKeys(
+      accountKeysFromLookups && {
+        accountKeysFromLookups,
+      }
+    );
     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..6ebe5478b82190 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({
             </td>
           </tr>
         )}
+
+        {version !== undefined && (
+          <tr>
+            <td>Transaction Version</td>
+            <td className="text-lg-end">{version}</td>
+          </tr>
+        )}
       </TableCardBody>
     </div>
   );
diff --git a/explorer/src/pages/inspector/AccountsCard.tsx b/explorer/src/pages/inspector/AccountsCard.tsx
index 53702ea3fb3405..b784df5fc62e7a 100644
--- a/explorer/src/pages/inspector/AccountsCard.tsx
+++ b/explorer/src/pages/inspector/AccountsCard.tsx
@@ -1,10 +1,10 @@
 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 +16,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,10 +30,10 @@ 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) => {
+    if (!message) return {accountRows: undefined, numAccounts: 0};
+    const staticAccountRows = message.staticAccountKeys.map((publicKey, accountIndex) => {
       const {
         numRequiredSignatures,
         numReadonlySignedAccounts,
@@ -47,7 +49,7 @@ export function AccountsCard({ message }: { message: Message }) {
         }
       } else if (
         accountIndex >=
-        message.accountKeys.length - numReadonlyUnsignedAccounts
+        message.staticAccountKeys.length - numReadonlyUnsignedAccounts
       ) {
         readOnly = true;
       }
@@ -61,6 +63,40 @@ export function AccountsCard({ message }: { message: Message }) {
 
       return <AccountRow key={accountIndex} {...props} />;
     });
+
+    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 <AccountFromLookupTableRow key={accountIndex} {...props} />;
+      });
+    });
+
+    const readonlyLookupTableRows = message.addressTableLookups.flatMap((lookup) => {
+      return lookup.readonlyIndexes.map(lookupTableIndex => {
+        const props = {
+          accountIndex,
+          lookupTableKey: lookup.accountKey,
+          lookupTableIndex,
+          readOnly: true,
+        };
+
+        accountIndex += 1;
+        return <AccountFromLookupTableRow key={accountIndex} {...props} />;
+      });
+    });
+
+    return {
+      accountRows: [...staticAccountRows, ...writableLookupTableRows, ...readonlyLookupTableRows],
+      numAccounts: accountIndex,
+    };
   }, [validMessage]);
 
   if (error) {
@@ -71,7 +107,7 @@ export function AccountsCard({ message }: { message: Message }) {
     <div className="card">
       <div className="card-header">
         <h3 className="card-header-title">
-          {`Account List (${message.accountKeys.length})`}
+          {`Account List (${numAccounts})`}
         </h3>
         <button
           className={`btn btn-sm d-flex ${
@@ -87,6 +123,36 @@ export function AccountsCard({ message }: { message: Message }) {
   );
 }
 
+function AccountFromLookupTableRow({
+  accountIndex,
+  lookupTableKey,
+  lookupTableIndex,
+  readOnly,
+}: {
+  accountIndex: number;
+  lookupTableKey: PublicKey;
+  lookupTableIndex: number;
+  readOnly: boolean;
+}) {
+  return (
+    <tr>
+      <td>
+        <div className="d-flex align-items-start flex-column">
+          Account #{accountIndex + 1}
+          <span className="mt-1">
+            {!readOnly && (
+              <span className="badge bg-danger-soft">Writable</span>
+            )}
+          </span>
+        </div>
+      </td>
+      <td className="text-lg-end">
+        <AddressFromLookupTableWithContext lookupTableKey={lookupTableKey} lookupTableIndex={lookupTableIndex} />
+      </td>
+    </tr>
+  );
+}
+
 function AccountRow({
   accountIndex,
   publicKey,
diff --git a/explorer/src/pages/inspector/AddressWithContext.tsx b/explorer/src/pages/inspector/AddressWithContext.tsx
index a943086066159b..394f8c7a10c966 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,36 @@ 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 <div>Loading Account from Lookup Table</div>;
+  } else if (lookupTableIndex < lookupTable.state.addresses.length) {
+    pubkey = lookupTable.state.addresses[lookupTableIndex];
+  } else {
+    return <div>Lookup Table Index is Invalid</div>;
+  }
+
+  return (
+    <div className="d-flex align-items-end flex-column">
+      <Address pubkey={pubkey} link />
+      <AccountInfo pubkey={pubkey} />
+    </div>
+  );
+}
+
 export function AddressWithContext({
   pubkey,
   validator,
diff --git a/explorer/src/pages/inspector/InspectorPage.tsx b/explorer/src/pages/inspector/InspectorPage.tsx
index a8ba07a7941404..604b0eba970ceb 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";
@@ -25,7 +25,7 @@ import base58 from "bs58";
 
 export type TransactionData = {
   rawMessage: Uint8Array;
-  message: Message;
+  message: VersionedMessage;
   signatures?: (string | null)[];
 };
 
@@ -117,7 +117,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,
@@ -294,7 +294,7 @@ function OverviewCard({
   raw,
   onClear,
 }: {
-  message: Message;
+  message: VersionedMessage;
   raw: Uint8Array;
   onClear: () => void;
 }) {
@@ -354,11 +354,11 @@ function OverviewCard({
               </div>
             </td>
             <td className="text-end">
-              {message.accountKeys.length === 0 ? (
+              {message.staticAccountKeys.length === 0 ? (
                 "No Fee Payer"
               ) : (
                 <AddressWithContext
-                  pubkey={message.accountKeys[0]}
+                  pubkey={message.staticAccountKeys[0]}
                   validator={feePayerValidator}
                 />
               )}
diff --git a/explorer/src/pages/inspector/InstructionsSection.tsx b/explorer/src/pages/inspector/InstructionsSection.tsx
index baba3a012bf28f..03618fd5ce6391 100644
--- a/explorer/src/pages/inspector/InstructionsSection.tsx
+++ b/explorer/src/pages/inspector/InstructionsSection.tsx
@@ -1,6 +1,5 @@
 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 { useCluster } from "providers/cluster";
@@ -9,10 +8,14 @@ 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 <InstructionCard key={index} {...{ message, ix, index }} />;
       })}
     </>
@@ -24,13 +27,13 @@ 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])
@@ -58,12 +61,12 @@ function InstructionCard({
             <td>Program</td>
             <td className="text-lg-end">
               <AddressWithContext
-                pubkey={message.accountKeys[ix.programIdIndex]}
+                pubkey={message.staticAccountKeys[ix.programIdIndex]}
                 validator={programValidator}
               />
             </td>
           </tr>
-          {ix.accounts.map((accountIndex, index) => {
+          {ix.accountKeyIndexes.map((accountIndex, index) => {
             return (
               <tr key={index}>
                 <td>
@@ -82,9 +85,13 @@ function InstructionCard({
                   </div>
                 </td>
                 <td className="text-lg-end">
-                  <AddressWithContext
-                    pubkey={message.accountKeys[accountIndex]}
-                  />
+                  {accountIndex < message.staticAccountKeys.length ? (
+                    <AddressWithContext
+                      pubkey={message.staticAccountKeys[accountIndex]}
+                    />
+                  ) : (
+                    "Context not yet available for address table lookups"
+                  )}
                 </td>
               </tr>
             );
@@ -94,7 +101,7 @@ function InstructionCard({
               Instruction Data <span className="text-muted">(Hex)</span>
             </td>
             <td className="text-lg-end">
-              <HexData raw={bs58.decode(ix.data)} />
+              <HexData raw={Buffer.from(ix.data)} />
             </td>
           </tr>
         </TableCardBody>
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<Array<InstructionLogs> | 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..a7b5d663eb09dc 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 import { pubkeyToString } from "utils";
-import { PublicKey, Connection, StakeActivationData } from "@solana/web3.js";
+import { PublicKey, Connection, StakeActivationData, AddressLookupTableAccount } from "@solana/web3.js";
 import { useCluster, Cluster } from "../cluster";
 import { HistoryProvider } from "./history";
 import { TokensProvider } from "./tokens";
@@ -19,6 +19,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 +77,11 @@ export type ConfigProgramData = {
   parsed: ConfigAccount;
 };
 
+export type AddressLookupTableProgramData = {
+  program: "address-lookup-table";
+  parsed: ParsedAddressLookupTableAccount;
+};
+
 export type ProgramData =
   | UpgradeableLoaderAccountData
   | StakeProgramData
@@ -83,7 +89,8 @@ export type ProgramData =
   | VoteProgramData
   | NonceProgramData
   | SysvarProgramData
-  | ConfigProgramData;
+  | ConfigProgramData
+  | AddressLookupTableProgramData;
 
 export interface Details {
   executable: boolean;
@@ -238,6 +245,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 +446,30 @@ export function useTokenAccountInfo(
   }
 }
 
+export function useAddressLookupTable(
+  address: string | undefined
+): AddressLookupTableAccount | undefined {
+  const accountInfo = useAccountInfo(address);
+  if (address === undefined) return;
+  if (accountInfo?.data?.details === undefined) return;
+  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 (rawData) {
+    return new AddressLookupTableAccount({
+      key,
+      state: AddressLookupTableAccount.deserialize(rawData),
+    });
+  }
+}
+
 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,
+});