From ba4a2d2241ad2e3be44c4d9e108a545d0af522bb Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 14 Aug 2022 15:50:23 +0100 Subject: [PATCH] Explorer: Add details page for address lookup table accounts (#27133) --- .../AddressLookupTableAccountSection.tsx | 84 +++++++++++++++++++ .../LookupTableEntriesCard.tsx | 60 +++++++++++++ .../account/address-lookup-table/types.ts | 13 +++ .../instruction/address-lookup-table/types.ts | 6 +- explorer/src/pages/AccountDetailsPage.tsx | 42 +++++++++- 5 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 explorer/src/components/account/address-lookup-table/AddressLookupTableAccountSection.tsx create mode 100644 explorer/src/components/account/address-lookup-table/LookupTableEntriesCard.tsx create mode 100644 explorer/src/components/account/address-lookup-table/types.ts diff --git a/explorer/src/components/account/address-lookup-table/AddressLookupTableAccountSection.tsx b/explorer/src/components/account/address-lookup-table/AddressLookupTableAccountSection.tsx new file mode 100644 index 0000000000..bffec1fa8b --- /dev/null +++ b/explorer/src/components/account/address-lookup-table/AddressLookupTableAccountSection.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { TableCardBody } from "components/common/TableCardBody"; +import { SolBalance } from "utils"; +import { Account, useFetchAccountInfo } from "providers/accounts"; +import { Address } from "components/common/Address"; +import { AddressLookupTableAccount } from "@solana/web3.js"; +import { Slot } from "components/common/Slot"; + +export function AddressLookupTableAccountSection({ + account, + data, +}: { + account: Account; + data: Uint8Array; +}) { + const lookupTableAccount = React.useMemo(() => { + return new AddressLookupTableAccount({ + key: account.pubkey, + state: AddressLookupTableAccount.deserialize(data), + }); + }, [account, data]); + const refresh = useFetchAccountInfo(); + return ( +
+
+

+ Address Lookup Table Account +

+ +
+ + + + Address + +
+ + + + Balance (SOL) + + + + + + Activation Status + + {lookupTableAccount.isActive() ? "Active" : "Deactivated"} + + + + Last Extended Slot + + {lookupTableAccount.state.lastExtendedSlot === 0 ? ( + "None (Empty)" + ) : ( + + )} + + + + Authority + + {lookupTableAccount.state.authority === undefined ? ( + "None (Frozen)" + ) : ( +
+ )} + + + +
+ ); +} diff --git a/explorer/src/components/account/address-lookup-table/LookupTableEntriesCard.tsx b/explorer/src/components/account/address-lookup-table/LookupTableEntriesCard.tsx new file mode 100644 index 0000000000..f20ff53b21 --- /dev/null +++ b/explorer/src/components/account/address-lookup-table/LookupTableEntriesCard.tsx @@ -0,0 +1,60 @@ +import React from "react"; + +import { AddressLookupTableAccount, PublicKey } from "@solana/web3.js"; +import { Address } from "components/common/Address"; + +export function LookupTableEntriesCard({ + lookupTableAccountData, +}: { + lookupTableAccountData: Uint8Array; +}) { + const lookupTableState = React.useMemo(() => { + return AddressLookupTableAccount.deserialize(lookupTableAccountData); + }, [lookupTableAccountData]); + + return ( +
+
+
+
+

Lookup Table Entries

+
+
+
+ +
+ + + + + + + + + {lookupTableState.addresses.length > 0 && + lookupTableState.addresses.map((entry: PublicKey, index) => { + return renderRow(entry, index); + })} + +
IndexAddress
+
+ + {lookupTableState.addresses.length === 0 && ( +
+
No entries found
+
+ )} +
+ ); +} + +const renderRow = (entry: PublicKey, index: number) => { + return ( + + {index} + +
+ + + ); +}; diff --git a/explorer/src/components/account/address-lookup-table/types.ts b/explorer/src/components/account/address-lookup-table/types.ts new file mode 100644 index 0000000000..136c412d50 --- /dev/null +++ b/explorer/src/components/account/address-lookup-table/types.ts @@ -0,0 +1,13 @@ +import { PublicKey } from "@solana/web3.js"; + +const PROGRAM_ID: string = "AddressLookupTab1e1111111111111111111111111"; + +export function isAddressLookupTableAccount( + accountOwner: PublicKey, + accountData: Uint8Array +): boolean { + if (accountOwner.toBase58() !== PROGRAM_ID) return false; + if (!accountData || accountData.length === 0) return false; + const LOOKUP_TABLE_ACCOUNT_TYPE = 1; + return accountData[0] === LOOKUP_TABLE_ACCOUNT_TYPE; +} diff --git a/explorer/src/components/instruction/address-lookup-table/types.ts b/explorer/src/components/instruction/address-lookup-table/types.ts index 4b1f01b794..9032b63d31 100644 --- a/explorer/src/components/instruction/address-lookup-table/types.ts +++ b/explorer/src/components/instruction/address-lookup-table/types.ts @@ -1,8 +1,6 @@ import { TransactionInstruction } from "@solana/web3.js"; -export const PROGRAM_IDS: string[] = [ - "AddressLookupTab1e1111111111111111111111111", -]; +const PROGRAM_ID: string = "AddressLookupTab1e1111111111111111111111111"; const INSTRUCTION_LOOKUP: { [key: number]: string } = { 0: "Create Lookup Table", @@ -15,7 +13,7 @@ const INSTRUCTION_LOOKUP: { [key: number]: string } = { export function isAddressLookupTableInstruction( instruction: TransactionInstruction ): boolean { - return PROGRAM_IDS.includes(instruction.programId.toBase58()); + return PROGRAM_ID === instruction.programId.toBase58(); } export function parseAddressLookupTableInstructionTitle( diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx index b9c0fadf76..1e69cc3d39 100644 --- a/explorer/src/pages/AccountDetailsPage.tsx +++ b/explorer/src/pages/AccountDetailsPage.tsx @@ -44,6 +44,9 @@ import { SecurityCard } from "components/account/SecurityCard"; import { AnchorAccountCard } from "components/account/AnchorAccountCard"; import { AnchorProgramCard } from "components/account/AnchorProgramCard"; import { useAnchorProgram } from "providers/anchor"; +import { isAddressLookupTableAccount } from "components/account/address-lookup-table/types"; +import { AddressLookupTableAccountSection } from "components/account/address-lookup-table/AddressLookupTableAccountSection"; +import { LookupTableEntriesCard } from "components/account/address-lookup-table/LookupTableEntriesCard"; const IDENTICON_WIDTH = 64; @@ -124,6 +127,13 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = { path: "/security", }, ], + "address-lookup-table": [ + { + slug: "entries", + title: "Table Entries", + path: "/entries", + }, + ], }; const TOKEN_TABS_HIDDEN = [ @@ -309,7 +319,8 @@ function DetailsSections({ } function InfoSection({ account }: { account: Account }) { - const data = account?.details?.data; + const details = account?.details; + const data = details?.data; if (data && data.program === "bpf-upgradeable-loader") { return ( @@ -342,6 +353,16 @@ function InfoSection({ account }: { account: Account }) { return ( ); + } else if ( + details?.rawData && + isAddressLookupTableAccount(details.owner, details.rawData) + ) { + return ( + + ); } else { return ; } @@ -374,7 +395,8 @@ export type MoreTabs = | "domains" | "security" | "anchor-program" - | "anchor-account"; + | "anchor-account" + | "entries"; function MoreSection({ account, @@ -386,7 +408,8 @@ function MoreSection({ tabs: (JSX.Element | null)[]; }) { const pubkey = account.pubkey; - const data = account?.details?.data; + const details = account?.details; + const data = details?.data; return ( <> @@ -456,6 +479,11 @@ function MoreSection({ )} + {tab === "entries" && + details?.rawData && + isAddressLookupTableAccount(details.owner, details.rawData) && ( + + )} ); } @@ -484,6 +512,14 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] { tabs.push(...TABS_LOOKUP[programTypeKey]); } + // Add the key for address lookup tables + if ( + account.details?.rawData && + isAddressLookupTableAccount(account.details.owner, account.details.rawData) + ) { + tabs.push(...TABS_LOOKUP["address-lookup-table"]); + } + // Add the key for Metaplex NFTs if ( data &&