From 3aaa5db8b0b01f6591e1c0e964ad8a4a86498e52 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 14:43:07 +0100 Subject: [PATCH 01/18] Show program metadata and idl --- app/address/[address]/idl/page.tsx | 27 +++ app/address/[address]/layout.tsx | 71 ++++++ .../[address]/program-metadata/page.tsx | 27 +++ app/components/account/IdlCard.tsx | 72 ++++++ .../account/ProgramMetadataCard.tsx | 224 ++++++++++++++++++ app/providers/anchor.tsx | 20 +- app/providers/idl.tsx | 123 ++++++++++ app/providers/program-metadata.tsx | 129 ++++++++++ 8 files changed, 686 insertions(+), 7 deletions(-) create mode 100644 app/address/[address]/idl/page.tsx create mode 100644 app/address/[address]/program-metadata/page.tsx create mode 100644 app/components/account/IdlCard.tsx create mode 100644 app/components/account/ProgramMetadataCard.tsx create mode 100644 app/providers/idl.tsx create mode 100644 app/providers/program-metadata.tsx diff --git a/app/address/[address]/idl/page.tsx b/app/address/[address]/idl/page.tsx new file mode 100644 index 000000000..3a80832cc --- /dev/null +++ b/app/address/[address]/idl/page.tsx @@ -0,0 +1,27 @@ +import { LoadingCard } from '@components/common/LoadingCard'; +import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; +import { Metadata } from 'next/types'; +import { Suspense } from 'react'; + +import { IdlCard } from '@/app/components/account/IdlCard'; + +type Props = Readonly<{ + params: { + address: string; + }; +}>; + +export async function generateMetadata(props: AddressPageMetadataProps): Promise { + return { + description: `The Interface Definition Language (IDL) file for the program at address ${props.params.address} on Solana`, + title: `Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`, + }; +} + +export default function AnchorProgramIDLPage({ params: { address } }: Props) { + return ( + }> + + + ); +} diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index 473f2baea..6f9f2d779 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -53,6 +53,7 @@ import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compresse import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig'; import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info'; import { MintAccountInfo } from '@/app/validators/accounts/token'; +import { useProgramMetadata } from '@/app/providers/program-metadata'; const IDENTICON_WIDTH = 64; @@ -559,6 +560,8 @@ export type MoreTabs = | 'domains' | 'security' | 'anchor-program' + | 'program-metadata' + | 'idl' | 'anchor-account' | 'entries' | 'concurrent-merkle-tree' @@ -733,6 +736,34 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) { tab: anchorProgramTab, }); + const idlTab: Tab = { + path: 'idl', + slug: 'idl', + title: 'IDL', + }; + tabComponents.push({ + component: ( + }> + + + ), + tab: idlTab, + }); + + const programMetadataTab: Tab = { + path: 'program-metadata', + slug: 'program-metadata', + title: 'Program Metadata', + }; + tabComponents.push({ + component: ( + }> + + + ), + tab: programMetadataTab, + }); + const accountDataTab: Tab = { path: 'anchor-account', slug: 'anchor-account', @@ -750,6 +781,27 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) { return tabComponents; } +function ProgramMetaDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { + const { url } = useCluster(); + const { ProgramMetaData } = useProgramMetadata(pubkey.toString(), url); + const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); + const selectedLayoutSegment = useSelectedLayoutSegment(); + const isActive = selectedLayoutSegment === tab.path; + + // Don't show the tab if there's no metadata + if (!programMetaData) { + return null; + } + + return ( +
  • + + {tab.title} + +
  • + ); +} + function AnchorProgramIdlLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { const { url } = useCluster(); const { idl } = useAnchorProgram(pubkey.toString(), url); @@ -769,6 +821,25 @@ function AnchorProgramIdlLink({ tab, address, pubkey }: { tab: Tab; address: str ); } +function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { + const { url } = useCluster(); + const { idl, program } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); + const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); + const selectedLayoutSegment = useSelectedLayoutSegment(); + const isActive = selectedLayoutSegment === tab.path; + if (!idl || program) { + return null; + } + + return ( +
  • + + {tab.title} + +
  • + ); +} + function AccountDataLink({ address, tab, programId }: { address: string; tab: Tab; programId: PublicKey }) { const { url } = useCluster(); const { program: accountAnchorProgram } = useAnchorProgram(programId.toString(), url); diff --git a/app/address/[address]/program-metadata/page.tsx b/app/address/[address]/program-metadata/page.tsx new file mode 100644 index 000000000..bcfa24e6e --- /dev/null +++ b/app/address/[address]/program-metadata/page.tsx @@ -0,0 +1,27 @@ +import { LoadingCard } from '@components/common/LoadingCard'; +import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; +import { Metadata } from 'next/types'; +import { Suspense } from 'react'; + +import { ProgramMetadataCard } from '@/app/components/account/ProgramMetadataCard'; + +type Props = Readonly<{ + params: { + address: string; + }; +}>; + +export async function generateMetadata(props: AddressPageMetadataProps): Promise { + return { + description: `This is the meta data uploaded by the program authority for program ${props.params.address} on Solana`, + title: `Program Metadata | ${await getReadableTitleFromAddress(props)} | Solana`, + }; +} + +export default function ProgramMetadataPage({ params: { address } }: Props) { + return ( + }> + + + ); +} diff --git a/app/components/account/IdlCard.tsx b/app/components/account/IdlCard.tsx new file mode 100644 index 000000000..88bad3008 --- /dev/null +++ b/app/components/account/IdlCard.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useCluster } from '@providers/cluster'; +import { useState } from 'react'; +import ReactJson from 'react-json-view'; + +import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; +import { getIdlSpecType } from '@/app/utils/convertLegacyIdl'; + +import { DownloadableButton } from '../common/Downloadable'; +import { IDLBadge } from '../common/IDLBadge'; + +export function IdlCard({ programId }: { programId: string }) { + const { url } = useCluster(); + const { idl } = useIdlFromProgramMetadataProgram(programId, url); + const [collapsedValue, setCollapsedValue] = useState(1); + + if (!idl) { + console.log('No IDL found'); + return null; + } + const spec = getIdlSpecType(idl); + + return ( +
    +
    +
    +
    +

    Program IDL (from Program Metadata PDA)

    +
    +
    + + Download IDL + +
    +
    +
    +
    + +
    + setCollapsedValue(e.target.checked ? false : 1)} + /> + +
    +
    + +
    + +
    +
    + ); +} diff --git a/app/components/account/ProgramMetadataCard.tsx b/app/components/account/ProgramMetadataCard.tsx new file mode 100644 index 000000000..a3ae813c2 --- /dev/null +++ b/app/components/account/ProgramMetadataCard.tsx @@ -0,0 +1,224 @@ +'use client'; + +import { useCluster } from '@providers/cluster'; +import { useState } from 'react'; +import ReactJson from 'react-json-view'; +import { ExternalLink } from 'react-feather'; + +import { useProgramMetadata } from '@/app/providers/program-metadata'; +import { DownloadableButton } from '../common/Downloadable'; +import { TableCardBody } from '../common/TableCardBody'; + +export function ProgramMetadataCard({ programId }: { programId: string }) { + const { url } = useCluster(); + const programMetaData = useProgramMetadata(programId, url); + const [collapsedValue, setCollapsedValue] = useState(1); + + if (!programMetaData) { + return ( +
    +
    +

    No Program Metadata Available

    +
    +
    + ); + } + + return ( +
    +
    +
    +
    +
    + {programMetaData.logo && ( + Program Logo + )} +

    {programMetaData.name}

    +
    +
    +
    +
    + + + {programMetaData.description && ( + + Description + {programMetaData.description} + + )} + + {programMetaData.notification && ( + + Notification + {programMetaData.notification} + + )} + + {programMetaData.version && ( + + Version + {programMetaData.version} + + )} + + {programMetaData.project_url && ( + + Project URL + + + + {programMetaData.project_url} + + + + + + )} + + {programMetaData.contacts && programMetaData.contacts.length > 0 && ( + + Contacts + +
      + {programMetaData.contacts.map((contact, index) => ( +
    • + {contact} +
    • + ))} +
    + + + )} + + {programMetaData.source_code && ( + + Source Code + + + + {programMetaData.source_code} + + + + + + )} + + {programMetaData.source_revision && ( + + Source Revision + {programMetaData.source_revision} + + )} + + {programMetaData.auditors && ( + + Auditors + + {Array.isArray(programMetaData.auditors) ? ( +
      + {programMetaData.auditors.map((auditor, index) => ( +
    • + {auditor} +
    • + ))} +
    + ) : ( + {programMetaData.auditors} + )} + + + )} + + {programMetaData.acknowledgements && ( + + Acknowledgements + {programMetaData.acknowledgements} + + )} + + {programMetaData.preferred_languages && programMetaData.preferred_languages.length > 0 && ( + + Preferred Languages + +
      + {programMetaData.preferred_languages.map((language, index) => ( +
    • + {language} +
    • + ))} +
    + + + )} + + {programMetaData.encryption && ( + + Encryption + {programMetaData.encryption} + + )} + + {programMetaData.expiry && ( + + Expiry + {programMetaData.expiry} + + )} +
    + +
    +
    +
    +

    Program Metadata (from Program Metadata PDA)

    +
    +
    + + Download Program Metadata + +
    +
    +
    +
    +
    + setCollapsedValue(e.target.checked ? false : 1)} + /> + +
    +
    + +
    + +
    +
    + ); +} diff --git a/app/providers/anchor.tsx b/app/providers/anchor.tsx index 1148fb10a..45d10512c 100644 --- a/app/providers/anchor.tsx +++ b/app/providers/anchor.tsx @@ -7,6 +7,7 @@ import { useEffect, useMemo } from 'react'; import { formatIdl } from '../utils/convertLegacyIdl'; import { useAccountInfo, useFetchAccountInfo } from './accounts'; +import { useIdlFromProgramMetadataProgram } from './idl'; const cachedAnchorProgramPromises: Record< string, @@ -112,21 +113,26 @@ function useIdlFromAnchorProgramSeed(programAddress: string, url: string): Idl | return cacheEntry.result; } -export function useAnchorProgram(programAddress: string, url: string): { program: Program | null; idl: Idl | null } { - // TODO(ngundotra): Rewrite this to be more efficient - // const idlFromBinary = useIdlFromSolanaProgramBinary(programAddress); +export function useAnchorProgram( + programAddress: string, + url: string +): { program: Program | null; idl: Idl | null } { + const idlFromMetadataProgram = useIdlFromProgramMetadataProgram(programAddress, url); const idlFromAnchorProgram = useIdlFromAnchorProgramSeed(programAddress, url); - const idl = idlFromAnchorProgram; - const program: Program | null = useMemo(() => { + + // First try anchor program IDL, then fall back to metadata program IDL + const idl = idlFromAnchorProgram || idlFromMetadataProgram?.idl; + + const program = useMemo(() => { if (!idl) return null; try { - const program = new Program(formatIdl(idl, programAddress), getProvider(url)); - return program; + return new Program(formatIdl(idl, programAddress), getProvider(url)); } catch (e) { console.error('Error creating anchor program for', programAddress, e, { idl }); return null; } }, [idl, programAddress, url]); + return { idl, program }; } diff --git a/app/providers/idl.tsx b/app/providers/idl.tsx new file mode 100644 index 000000000..cb3c1d63c --- /dev/null +++ b/app/providers/idl.tsx @@ -0,0 +1,123 @@ +import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; +import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import pako from 'pako'; +import { useMemo } from 'react'; + +import { formatIdl } from '../utils/convertLegacyIdl'; + +const cachedIdlPromises: Record< + string, + void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: Idl | null } +> = {}; + +async function fetchIdlFromMetadataProgram(programAddress: string, connection: Connection): Promise { + const [idlAccount] = PublicKey.findProgramAddressSync( + [Buffer.from('idl', 'utf8'), new PublicKey(programAddress).toBuffer()], + new PublicKey('pmetaypqG6SiB47xMigYVMAkuHDWeSDXcv3zzDrJJvA') + ); + + const accountInfo = await connection.getAccountInfo(idlAccount); + if (!accountInfo) { + console.error('IDL account not found!'); + return null; + } + + // Extract data length and compressed data + const dataLenBytes = accountInfo.data.slice(40, 44); // `data_len` starts at offset 40 + const dataLength = new DataView(dataLenBytes.buffer).getUint32(0, true); // Little-endian + const compressedData = accountInfo.data.slice(44, 44 + dataLength); // Skip metadata (44 bytes) + + // Decompress and parse the data + try { + const decompressedString = new TextDecoder('utf-8').decode(pako.inflate(compressedData)); + // First try parsing as IDL directly + try { + const idl = JSON.parse(decompressedString); + return idl; + } catch (parseErr) { + console.log('Not a direct IDL object, checking if URL...'); + } + + // If not an IDL, try handling as URL + try { + const url = new URL(decompressedString.trim()); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch IDL from URL: ${response.statusText}`); + } + const fetchedIdl = await response.json(); + return fetchedIdl; + } catch (urlErr) { + console.error('Not a valid IDL URL:', urlErr); + } + + console.error('Could not parse IDL from decompressed data'); + return null; + } catch (err) { + console.error('Failed to decompress or process data:', err); + return null; + } +} + +function useIdlFromMetadataProgram(programAddress: string, url: string): Idl | null { + const key = `${programAddress}-${url}`; + const cacheEntry = cachedIdlPromises[key]; + + // If there's no cached entry, start fetching the IDL + if (cacheEntry === undefined) { + const connection = new Connection(url); + const promise = fetchIdlFromMetadataProgram(programAddress, connection) + .then(idl => { + cachedIdlPromises[key] = { + __type: 'result', + result: idl, + }; + }) + .catch(err => { + console.error('Error fetching IDL:', err); + cachedIdlPromises[key] = { __type: 'result', result: null }; + }); + cachedIdlPromises[key] = { + __type: 'promise', + promise, + }; + throw promise; // Throw the promise for React Suspense + } + + // If the cache has a pending promise, throw it + if (cacheEntry.__type === 'promise') { + throw cacheEntry.promise; + } + + // Return the cached result + return cacheEntry.result; +} + +function getProvider(url: string) { + return new AnchorProvider(new Connection(url), new NodeWallet(Keypair.generate()), {}); +} + +export function useIdlFromProgramMetadataProgram( + programAddress: string, + url: string +): { program: Program | null; idl: Idl | null } { + const idlFromProgramMetadataProgram = useIdlFromMetadataProgram(programAddress, url); + const idl = idlFromProgramMetadataProgram; + const program: Program | null = useMemo(() => { + if (!idl) return null; + try { + const program = new Program(formatIdl(idl, programAddress), getProvider(url)); + return program; + } catch (e) { + console.error('Error creating anchor program for', programAddress, e, { idl }); + return null; + } + }, [idl, programAddress, url]); + return { idl, program }; +} + +export type AnchorAccount = { + layout: string; + account: object; +}; diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx new file mode 100644 index 000000000..e3f1032e2 --- /dev/null +++ b/app/providers/program-metadata.tsx @@ -0,0 +1,129 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import pako from 'pako'; + +const cachedLogoProgramPromises: Record< + string, + void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: ProgramMetaData | null } +> = {}; + +interface ProgramMetaData { + name: string; + logo?: string; + description?: string; + notification?: string; + sdk?: string; + version?: string; + project_url?: string; + contacts?: string[]; + policy?: string; + preferred_languages?: string[]; + encryption?: string; + source_code?: string; + source_release?: string; + source_revision?: string; + auditors?: string[] | string; + acknowledgements?: string; + expiry?: string; +} + +async function fetchProgramMetaData(programAddress: string, connection: Connection): Promise { + const [idlAccount] = await PublicKey.findProgramAddress( + [Buffer.from('metadata', 'utf8'), new PublicKey(programAddress).toBuffer()], + new PublicKey('pmetaypqG6SiB47xMigYVMAkuHDWeSDXcv3zzDrJJvA') + ); + console.log(`IDl account ${programAddress} idlAccount: ${idlAccount.toBase58()}`); + + const accountInfo = await connection.getAccountInfo(idlAccount); + if (!accountInfo) { + console.error('IDL account not found!'); + return null; + } + + // Extract data length and compressed data + const dataLenBytes = accountInfo.data.slice(40, 44); // `data_len` starts at offset 40 + const dataLength = new DataView(dataLenBytes.buffer).getUint32(0, true); // Little-endian + const compressedData = accountInfo.data.slice(44, 44 + dataLength); // Skip metadata (44 bytes) + + // Decompress and parse the metadata + try { + const decompressedString = new TextDecoder('utf-8').decode(pako.inflate(compressedData)); + + // First try parsing as metadata object directly + try { + const metadata = JSON.parse(decompressedString) as ProgramMetaData; + if (metadata.name) { // Basic validation that it's a metadata object + return metadata; + } + } catch (parseErr) { + console.log('Not a direct metadata object, checking if URL...'); + } + + // Then try handling as URL + try { + const url = new URL(decompressedString.trim()); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch metadata from URL: ${response.statusText}`); + } + const fetchedMetadata = await response.json(); + if (fetchedMetadata.name) { // Basic validation + return fetchedMetadata; + } + throw new Error('Fetched data is not a valid metadata object'); + } catch (urlErr) { + console.log('Not a valid metadata URL'); + } + + // If we get here, neither approach worked + console.error('Could not parse metadata from decompressed data'); + return null; + + } catch (err) { + console.error('Failed to decompress or process metadata:', err); + return null; + } +} + +function useProgramMetaData(programAddress: string, url: string): ProgramMetaData | null { + const key = `${programAddress}-${url}-logo`; + const cacheEntry = cachedLogoProgramPromises[key]; + + // If there's no cached entry, start fetching the IDL + if (cacheEntry === undefined) { + const connection = new Connection(url); + const promise = fetchProgramMetaData(programAddress, connection) + .then(idl => { + cachedLogoProgramPromises[key] = { + __type: 'result', + result: idl, + }; + }) + .catch(err => { + console.error('Error fetching IDL:', err); + cachedLogoProgramPromises[key] = { __type: 'result', result: null }; + }); + cachedLogoProgramPromises[key] = { + __type: 'promise', + promise, + }; + throw promise; // Throw the promise for React Suspense + } + + // If the cache has a pending promise, throw it + if (cacheEntry.__type === 'promise') { + throw cacheEntry.promise; + } + + // Return the cached result + return cacheEntry.result; +} + +export function useProgramMetadata(programAddress: string, url: string): ProgramMetaData | null { + const programMetaData = useProgramMetaData(programAddress, url); + return programMetaData; +} + +export type AnchorAccount = { + layout: string; + account: object; +}; From 0e597aa65bd67f1935f387e81d5082b78805d54b Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 14:45:34 +0100 Subject: [PATCH 02/18] Update layout.tsx --- app/address/[address]/layout.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index 6f9f2d779..e5a6002d5 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -54,6 +54,7 @@ import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig'; import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info'; import { MintAccountInfo } from '@/app/validators/accounts/token'; import { useProgramMetadata } from '@/app/providers/program-metadata'; +import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; const IDENTICON_WIDTH = 64; @@ -783,13 +784,13 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) { function ProgramMetaDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { const { url } = useCluster(); - const { ProgramMetaData } = useProgramMetadata(pubkey.toString(), url); + const programMetadata = useProgramMetadata(pubkey.toString(), url); const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); const selectedLayoutSegment = useSelectedLayoutSegment(); const isActive = selectedLayoutSegment === tab.path; // Don't show the tab if there's no metadata - if (!programMetaData) { + if (!programMetadata) { return null; } From ed4ad0b50f5749c95b2d0b8e0c8c4b28cc91aab3 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 15:03:35 +0100 Subject: [PATCH 03/18] Update program-metadata.tsx --- app/providers/program-metadata.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx index e3f1032e2..50ba3e314 100644 --- a/app/providers/program-metadata.tsx +++ b/app/providers/program-metadata.tsx @@ -51,7 +51,8 @@ async function fetchProgramMetaData(programAddress: string, connection: Connecti // First try parsing as metadata object directly try { const metadata = JSON.parse(decompressedString) as ProgramMetaData; - if (metadata.name) { // Basic validation that it's a metadata object + if (metadata.name) { + // Basic validation that it's a metadata object return metadata; } } catch (parseErr) { @@ -66,7 +67,8 @@ async function fetchProgramMetaData(programAddress: string, connection: Connecti throw new Error(`Failed to fetch metadata from URL: ${response.statusText}`); } const fetchedMetadata = await response.json(); - if (fetchedMetadata.name) { // Basic validation + if (fetchedMetadata.name) { + // Basic validation return fetchedMetadata; } throw new Error('Fetched data is not a valid metadata object'); @@ -77,7 +79,6 @@ async function fetchProgramMetaData(programAddress: string, connection: Connecti // If we get here, neither approach worked console.error('Could not parse metadata from decompressed data'); return null; - } catch (err) { console.error('Failed to decompress or process metadata:', err); return null; @@ -99,7 +100,7 @@ function useProgramMetaData(programAddress: string, url: string): ProgramMetaDat }; }) .catch(err => { - console.error('Error fetching IDL:', err); + console.error('Error fetching Program Metadata:', err); cachedLogoProgramPromises[key] = { __type: 'result', result: null }; }); cachedLogoProgramPromises[key] = { From 4bd0f3633c28192d615d39c347dda4dace8a8bf3 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 15:09:03 +0100 Subject: [PATCH 04/18] Fix linting --- app/address/[address]/layout.tsx | 4 ++-- app/components/account/ProgramMetadataCard.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index e5a6002d5..b334d941b 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -50,11 +50,11 @@ import { Address } from 'web3js-experimental'; import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard'; import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft'; +import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; +import { useProgramMetadata } from '@/app/providers/program-metadata'; import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig'; import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info'; import { MintAccountInfo } from '@/app/validators/accounts/token'; -import { useProgramMetadata } from '@/app/providers/program-metadata'; -import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; const IDENTICON_WIDTH = 64; diff --git a/app/components/account/ProgramMetadataCard.tsx b/app/components/account/ProgramMetadataCard.tsx index a3ae813c2..2f5ae38c7 100644 --- a/app/components/account/ProgramMetadataCard.tsx +++ b/app/components/account/ProgramMetadataCard.tsx @@ -2,10 +2,11 @@ import { useCluster } from '@providers/cluster'; import { useState } from 'react'; -import ReactJson from 'react-json-view'; import { ExternalLink } from 'react-feather'; +import ReactJson from 'react-json-view'; import { useProgramMetadata } from '@/app/providers/program-metadata'; + import { DownloadableButton } from '../common/Downloadable'; import { TableCardBody } from '../common/TableCardBody'; From 6f861c35e20da662ae444c9d4d0823206b13cb69 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 16:55:25 +0100 Subject: [PATCH 05/18] Always show idl tab --- app/address/[address]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index b334d941b..3a8cd6149 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -828,7 +828,7 @@ function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubk const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); const selectedLayoutSegment = useSelectedLayoutSegment(); const isActive = selectedLayoutSegment === tab.path; - if (!idl || program) { + if (!idl) { return null; } From 898735baba57abbc8d6933b4e0d283b1fdce86ff Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 4 Dec 2024 16:58:04 +0100 Subject: [PATCH 06/18] Update layout.tsx --- app/address/[address]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index 3a8cd6149..4864bbb98 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -824,7 +824,7 @@ function AnchorProgramIdlLink({ tab, address, pubkey }: { tab: Tab; address: str function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { const { url } = useCluster(); - const { idl, program } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); + const { idl } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); const selectedLayoutSegment = useSelectedLayoutSegment(); const isActive = selectedLayoutSegment === tab.path; From 719e6b645973f0a767eb9c072c41a928ae716ef8 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 10 Dec 2024 22:56:20 +0100 Subject: [PATCH 07/18] Use solana-program-metadata package --- app/providers/idl.tsx | 49 +++-------------------- app/providers/program-metadata.tsx | 54 ++----------------------- package.json | 1 + pnpm-lock.yaml | 63 ++++++++++++++++++++++++++---- 4 files changed, 66 insertions(+), 101 deletions(-) diff --git a/app/providers/idl.tsx b/app/providers/idl.tsx index cb3c1d63c..3be12f5da 100644 --- a/app/providers/idl.tsx +++ b/app/providers/idl.tsx @@ -1,8 +1,8 @@ import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; -import pako from 'pako'; import { useMemo } from 'react'; +import { fetchIDL } from 'solana-program-metadata'; import { formatIdl } from '../utils/convertLegacyIdl'; @@ -12,52 +12,15 @@ const cachedIdlPromises: Record< > = {}; async function fetchIdlFromMetadataProgram(programAddress: string, connection: Connection): Promise { - const [idlAccount] = PublicKey.findProgramAddressSync( - [Buffer.from('idl', 'utf8'), new PublicKey(programAddress).toBuffer()], - new PublicKey('pmetaypqG6SiB47xMigYVMAkuHDWeSDXcv3zzDrJJvA') - ); + const result = await fetchIDL(new PublicKey(programAddress), connection.rpcEndpoint); - const accountInfo = await connection.getAccountInfo(idlAccount); - if (!accountInfo) { - console.error('IDL account not found!'); + if (!result) { + console.error('IDL not found!'); return null; } - // Extract data length and compressed data - const dataLenBytes = accountInfo.data.slice(40, 44); // `data_len` starts at offset 40 - const dataLength = new DataView(dataLenBytes.buffer).getUint32(0, true); // Little-endian - const compressedData = accountInfo.data.slice(44, 44 + dataLength); // Skip metadata (44 bytes) - - // Decompress and parse the data - try { - const decompressedString = new TextDecoder('utf-8').decode(pako.inflate(compressedData)); - // First try parsing as IDL directly - try { - const idl = JSON.parse(decompressedString); - return idl; - } catch (parseErr) { - console.log('Not a direct IDL object, checking if URL...'); - } - - // If not an IDL, try handling as URL - try { - const url = new URL(decompressedString.trim()); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch IDL from URL: ${response.statusText}`); - } - const fetchedIdl = await response.json(); - return fetchedIdl; - } catch (urlErr) { - console.error('Not a valid IDL URL:', urlErr); - } - - console.error('Could not parse IDL from decompressed data'); - return null; - } catch (err) { - console.error('Failed to decompress or process data:', err); - return null; - } + const idl = JSON.parse(result); + return idl; } function useIdlFromMetadataProgram(programAddress: string, url: string): Idl | null { diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx index 50ba3e314..10a7eba1d 100644 --- a/app/providers/program-metadata.tsx +++ b/app/providers/program-metadata.tsx @@ -1,5 +1,6 @@ import { Connection, PublicKey } from '@solana/web3.js'; import pako from 'pako'; +import { fetchProgramMetadata } from 'solana-program-metadata'; const cachedLogoProgramPromises: Record< string, @@ -27,58 +28,9 @@ interface ProgramMetaData { } async function fetchProgramMetaData(programAddress: string, connection: Connection): Promise { - const [idlAccount] = await PublicKey.findProgramAddress( - [Buffer.from('metadata', 'utf8'), new PublicKey(programAddress).toBuffer()], - new PublicKey('pmetaypqG6SiB47xMigYVMAkuHDWeSDXcv3zzDrJJvA') - ); - console.log(`IDl account ${programAddress} idlAccount: ${idlAccount.toBase58()}`); - - const accountInfo = await connection.getAccountInfo(idlAccount); - if (!accountInfo) { - console.error('IDL account not found!'); - return null; - } - - // Extract data length and compressed data - const dataLenBytes = accountInfo.data.slice(40, 44); // `data_len` starts at offset 40 - const dataLength = new DataView(dataLenBytes.buffer).getUint32(0, true); // Little-endian - const compressedData = accountInfo.data.slice(44, 44 + dataLength); // Skip metadata (44 bytes) - - // Decompress and parse the metadata try { - const decompressedString = new TextDecoder('utf-8').decode(pako.inflate(compressedData)); - - // First try parsing as metadata object directly - try { - const metadata = JSON.parse(decompressedString) as ProgramMetaData; - if (metadata.name) { - // Basic validation that it's a metadata object - return metadata; - } - } catch (parseErr) { - console.log('Not a direct metadata object, checking if URL...'); - } - - // Then try handling as URL - try { - const url = new URL(decompressedString.trim()); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch metadata from URL: ${response.statusText}`); - } - const fetchedMetadata = await response.json(); - if (fetchedMetadata.name) { - // Basic validation - return fetchedMetadata; - } - throw new Error('Fetched data is not a valid metadata object'); - } catch (urlErr) { - console.log('Not a valid metadata URL'); - } - - // If we get here, neither approach worked - console.error('Could not parse metadata from decompressed data'); - return null; + const programMetadata = await fetchProgramMetadata(new PublicKey(programAddress), connection.rpcEndpoint); + return programMetadata; } catch (err) { console.error('Failed to decompress or process metadata:', err); return null; diff --git a/package.json b/package.json index 33bf5f1fd..38f0bb6e2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@solana/web3.js": "^1.66.6", "@solflare-wallet/utl-sdk": "^1.4.0", "@types/bn.js": "5.1.0", + "solana-program-metadata": "^0.0.14", "axios": "^0.28.0", "bignumber.js": "^9.0.2", "bn.js": "5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46bb165aa..f4a96e0e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ importers: react-select: specifier: ^4.3.1 version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + solana-program-metadata: + specifier: ^0.0.14 + version: 0.0.14(bufferutil@4.0.7)(utf-8-validate@5.0.10) superstruct: specifier: ^0.15.3 version: 0.15.3 @@ -1646,6 +1649,9 @@ packages: '@solana/web3.js@1.95.3': resolution: {integrity: sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og==} + '@solana/web3.js@1.95.8': + resolution: {integrity: sha512-sBHzNh7dHMrmNS5xPD1d0Xa2QffW/RXaxu/OysRXBfwTp+LYqGGmMtCYYwrHPrN5rjAmJCsQRNAwv4FM0t3B6g==} + '@solana/web3.js@2.0.0': resolution: {integrity: sha512-x+ZRB2/r5tVK/xw8QRbAfgPcX51G9f2ifEyAQ/J5npOO+6+MPeeCjtr5UxHNDAYs9Ypo0PN+YJATCO4vhzQJGg==} engines: {node: '>=20.18.0'} @@ -2459,6 +2465,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -4524,6 +4534,10 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + solana-program-metadata@0.0.14: + resolution: {integrity: sha512-9dWi3JCi7HZhjH4Kj4041DIqRQo9ixXefFcHq26Y/L4T/efM11GIRNGAEfrU0PDo7CrJLyhZXhN80OVH1U4b6A==} + hasBin: true + source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -6545,7 +6559,7 @@ snapshots: dependencies: '@metaplex-foundation/mpl-core': 0.0.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/spl-token': 0.1.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.95.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -6589,7 +6603,7 @@ snapshots: '@metaplex-foundation/mpl-token-metadata': 0.0.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@metaplex-foundation/mpl-token-vault': 0.0.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/spl-token': 0.1.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.95.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -6599,7 +6613,7 @@ snapshots: dependencies: '@metaplex-foundation/mpl-core': 0.0.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/spl-token': 0.1.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.95.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -6634,7 +6648,7 @@ snapshots: dependencies: '@metaplex-foundation/mpl-core': 0.0.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/spl-token': 0.1.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.95.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -7605,6 +7619,28 @@ snapshots: - encoding - utf-8-validate + '@solana/web3.js@1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.25.6 + '@noble/curves': 1.5.0 + '@noble/hashes': 1.5.0 + '@solana/buffer-layout': 4.0.1 + agentkeepalive: 4.5.0 + bigint-buffer: 1.1.5 + bn.js: 5.2.1 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.1.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) + node-fetch: 2.6.9 + rpc-websockets: 9.0.2 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + '@solana/web3.js@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.0.4)(ws@7.5.10(bufferutil@4.0.7)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.0.4) @@ -8587,6 +8623,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@11.1.0: {} + commander@12.1.0: {} commander@2.20.3: {} @@ -9004,7 +9042,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.13.0 eslint: 8.39.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.39.0))(eslint@8.39.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) get-tsconfig: 4.5.0 globby: 13.1.4 @@ -9017,7 +9055,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.39.0))(eslint@8.39.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -9038,7 +9076,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.39.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.39.0))(eslint@8.39.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -11174,6 +11212,17 @@ snapshots: dot-case: 3.0.4 tslib: 2.5.0 + solana-program-metadata@0.0.14(bufferutil@4.0.7)(utf-8-validate@5.0.10): + dependencies: + '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) + commander: 11.1.0 + pako: 2.1.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + source-map-js@1.0.2: {} source-map-resolve@0.6.0: From 848212d1c03357cba47f518191e3b54997cb1f00 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 10 Dec 2024 23:00:08 +0100 Subject: [PATCH 08/18] lint fix --- app/providers/program-metadata.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx index 10a7eba1d..4a93bc468 100644 --- a/app/providers/program-metadata.tsx +++ b/app/providers/program-metadata.tsx @@ -1,5 +1,4 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import pako from 'pako'; import { fetchProgramMetadata } from 'solana-program-metadata'; const cachedLogoProgramPromises: Record< From 6645c4bf8765d0b4a4d1c2f1ca0773f34480b49f Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Wed, 11 Dec 2024 14:07:51 +0100 Subject: [PATCH 09/18] Update to newest metadata package --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 38f0bb6e2..9b3d6e385 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@solana/web3.js": "^1.66.6", "@solflare-wallet/utl-sdk": "^1.4.0", "@types/bn.js": "5.1.0", - "solana-program-metadata": "^0.0.14", + "solana-program-metadata": "^0.0.15", "axios": "^0.28.0", "bignumber.js": "^9.0.2", "bn.js": "5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4a96e0e0..3b8e96bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: specifier: ^4.3.1 version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) solana-program-metadata: - specifier: ^0.0.14 - version: 0.0.14(bufferutil@4.0.7)(utf-8-validate@5.0.10) + specifier: ^0.0.15 + version: 0.0.15(bufferutil@4.0.7)(utf-8-validate@5.0.10) superstruct: specifier: ^0.15.3 version: 0.15.3 @@ -4534,8 +4534,8 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - solana-program-metadata@0.0.14: - resolution: {integrity: sha512-9dWi3JCi7HZhjH4Kj4041DIqRQo9ixXefFcHq26Y/L4T/efM11GIRNGAEfrU0PDo7CrJLyhZXhN80OVH1U4b6A==} + solana-program-metadata@0.0.15: + resolution: {integrity: sha512-EHFssAgLWcEgJNTxq6lFLmq2fH129lEjRLv9srin0biEqAGh+kDGygC2IiqIs0c9mJxZTfCrqSvhlRHB9zXsFA==} hasBin: true source-map-js@1.0.2: @@ -11212,7 +11212,7 @@ snapshots: dot-case: 3.0.4 tslib: 2.5.0 - solana-program-metadata@0.0.14(bufferutil@4.0.7)(utf-8-validate@5.0.10): + solana-program-metadata@0.0.15(bufferutil@4.0.7)(utf-8-validate@5.0.10): dependencies: '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) From df699c84c3c7d4361d9239d31e4073773067de80 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Thu, 12 Dec 2024 14:32:26 +0100 Subject: [PATCH 10/18] Update solana-program-metadata --- app/providers/program-metadata.tsx | 28 ++++------------------------ package.json | 2 +- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx index 4a93bc468..14bc3e629 100644 --- a/app/providers/program-metadata.tsx +++ b/app/providers/program-metadata.tsx @@ -1,31 +1,11 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import { fetchProgramMetadata } from 'solana-program-metadata'; +import { fetchProgramMetadata, ProgramMetaData } from 'solana-program-metadata'; const cachedLogoProgramPromises: Record< string, void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: ProgramMetaData | null } > = {}; -interface ProgramMetaData { - name: string; - logo?: string; - description?: string; - notification?: string; - sdk?: string; - version?: string; - project_url?: string; - contacts?: string[]; - policy?: string; - preferred_languages?: string[]; - encryption?: string; - source_code?: string; - source_release?: string; - source_revision?: string; - auditors?: string[] | string; - acknowledgements?: string; - expiry?: string; -} - async function fetchProgramMetaData(programAddress: string, connection: Connection): Promise { try { const programMetadata = await fetchProgramMetadata(new PublicKey(programAddress), connection.rpcEndpoint); @@ -40,14 +20,14 @@ function useProgramMetaData(programAddress: string, url: string): ProgramMetaDat const key = `${programAddress}-${url}-logo`; const cacheEntry = cachedLogoProgramPromises[key]; - // If there's no cached entry, start fetching the IDL + // If there's no cached entry, start fetching the program metadata if (cacheEntry === undefined) { const connection = new Connection(url); const promise = fetchProgramMetaData(programAddress, connection) - .then(idl => { + .then(programMetadata => { cachedLogoProgramPromises[key] = { __type: 'result', - result: idl, + result: programMetadata, }; }) .catch(err => { diff --git a/package.json b/package.json index 9b3d6e385..fa35604f2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@solana/web3.js": "^1.66.6", "@solflare-wallet/utl-sdk": "^1.4.0", "@types/bn.js": "5.1.0", - "solana-program-metadata": "^0.0.15", + "solana-program-metadata": "^0.0.17", "axios": "^0.28.0", "bignumber.js": "^9.0.2", "bn.js": "5.2.1", From be6ff3f64f1f04fc16925b55d7d2bbdff3d0b716 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 17 Dec 2024 13:50:54 +0100 Subject: [PATCH 11/18] Code review. Move IDLs into their own tab --- README.md | 9 +-- app/address/[address]/anchor-program/page.tsx | 26 ------- app/address/[address]/idl/page.tsx | 66 +++++++++++------ app/address/[address]/layout.tsx | 40 ++--------- app/components/account/AnchorProgramCard.tsx | 71 ------------------- app/components/account/IdlCard.tsx | 13 ++-- package.json | 2 +- 7 files changed, 63 insertions(+), 164 deletions(-) delete mode 100644 app/address/[address]/anchor-program/page.tsx delete mode 100644 app/components/account/AnchorProgramCard.tsx diff --git a/README.md b/README.md index 78858ef63..8abfc9b5c 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ ## Development -Contributing to the Explorer requires `pnpm` version `9.10.0`. +Contributing to the Explorer requires `pnpm` version `9.10.0`. Once you have this version of `pnpm`, you can continue with the following steps. - - Copy `.env.example` into `.env` & fill out the fields with custom RPC urls \ from a Solana RPC provider. You should not use `https://api.mainnet-beta.solana.com` \ or `https://api.devnet.solana.com` or else you will get rate-limited. These are public \ @@ -28,17 +27,19 @@ Once you have this version of `pnpm`, you can continue with the following steps. The page will reload if you make edits. \ You will also see any lint errors in the console. +- `npm run lint -- --fix` \ + Lints the code and fixes any linting errors. + - (Optional) `pnpm test` \ Launches the test runner in the interactive watch mode.
    ## Troubleshooting -Still can't run the explorer with `pnpm dev`? +Still can't run the explorer with `pnpm dev`? Seeing sass dependency errors? Make sure you have `pnpm` version `9.10.0`, `git stash` your changes, then blow reset to master with `rm -rf node_modules && git reset --hard HEAD`. Now running `pnpm i` followed by `pnpm dev` should work. If it is working, don't forget to reapply your changes with `git stash pop`. - # Disclaimer All claims, content, designs, algorithms, estimates, roadmaps, diff --git a/app/address/[address]/anchor-program/page.tsx b/app/address/[address]/anchor-program/page.tsx deleted file mode 100644 index 986b55208..000000000 --- a/app/address/[address]/anchor-program/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { AnchorProgramCard } from '@components/account/AnchorProgramCard'; -import { LoadingCard } from '@components/common/LoadingCard'; -import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; -import { Metadata } from 'next/types'; -import { Suspense } from 'react'; - -type Props = Readonly<{ - params: { - address: string; - }; -}>; - -export async function generateMetadata(props: AddressPageMetadataProps): Promise { - return { - description: `The Interface Definition Language (IDL) file for the Anchor program at address ${props.params.address} on Solana`, - title: `Anchor Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`, - }; -} - -export default function AnchorProgramIDLPage({ params: { address } }: Props) { - return ( - }> - - - ); -} diff --git a/app/address/[address]/idl/page.tsx b/app/address/[address]/idl/page.tsx index 3a80832cc..9054f5747 100644 --- a/app/address/[address]/idl/page.tsx +++ b/app/address/[address]/idl/page.tsx @@ -1,27 +1,51 @@ -import { LoadingCard } from '@components/common/LoadingCard'; -import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; -import { Metadata } from 'next/types'; -import { Suspense } from 'react'; +'use client'; -import { IdlCard } from '@/app/components/account/IdlCard'; +import { IdlCard } from '@components/account/IdlCard'; +import { useAnchorProgram } from '@providers/anchor'; +import { useCluster } from '@providers/cluster'; +import { useIdlFromProgramMetadataProgram } from '@providers/idl'; +import { useState } from 'react'; -type Props = Readonly<{ - params: { - address: string; - }; -}>; +export default function IdlPage({ params: { address } }: { params: { address: string } }) { + const { url } = useCluster(); + const [activeTab, setActiveTab] = useState<'anchor' | 'metadata'>('anchor'); + const { idl: anchorIdl } = useAnchorProgram(address, url); + const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(address, url); -export async function generateMetadata(props: AddressPageMetadataProps): Promise { - return { - description: `The Interface Definition Language (IDL) file for the program at address ${props.params.address} on Solana`, - title: `Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`, - }; -} - -export default function AnchorProgramIDLPage({ params: { address } }: Props) { return ( - }> - - +
    +
    +
      + {anchorIdl && ( +
    • + +
    • + )} + {metadataIdl && ( +
    • + +
    • + )} +
    +
    +
    + {activeTab === 'anchor' && anchorIdl && ( + + )} + {activeTab === 'metadata' && metadataIdl && ( + + )} +
    +
    ); } diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index 4864bbb98..f25b93dbc 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -560,7 +560,6 @@ export type MoreTabs = | 'attributes' | 'domains' | 'security' - | 'anchor-program' | 'program-metadata' | 'idl' | 'anchor-account' @@ -723,20 +722,6 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) { tab: programMultisigTab, }); - const anchorProgramTab: Tab = { - path: 'anchor-program', - slug: 'anchor-program', - title: 'Anchor Program IDL', - }; - tabComponents.push({ - component: ( - }> - - - ), - tab: anchorProgramTab, - }); - const idlTab: Tab = { path: 'idl', slug: 'idl', @@ -803,32 +788,15 @@ function ProgramMetaDataLink({ tab, address, pubkey }: { tab: Tab; address: stri ); } -function AnchorProgramIdlLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { - const { url } = useCluster(); - const { idl } = useAnchorProgram(pubkey.toString(), url); - const anchorProgramPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); - const selectedLayoutSegment = useSelectedLayoutSegment(); - const isActive = selectedLayoutSegment === tab.path; - if (!idl) { - return null; - } - - return ( -
  • - - {tab.title} - -
  • - ); -} - function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { const { url } = useCluster(); - const { idl } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); + const { idl: anchorIdl } = useAnchorProgram(pubkey.toString(), url); + const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); const selectedLayoutSegment = useSelectedLayoutSegment(); const isActive = selectedLayoutSegment === tab.path; - if (!idl) { + + if (!anchorIdl && !metadataIdl) { return null; } diff --git a/app/components/account/AnchorProgramCard.tsx b/app/components/account/AnchorProgramCard.tsx deleted file mode 100644 index 264222956..000000000 --- a/app/components/account/AnchorProgramCard.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; - -import { useAnchorProgram } from '@providers/anchor'; -import { useCluster } from '@providers/cluster'; -import { useState } from 'react'; -import ReactJson from 'react-json-view'; - -import { getIdlSpecType } from '@/app/utils/convertLegacyIdl'; - -import { DownloadableButton } from '../common/Downloadable'; -import { IDLBadge } from '../common/IDLBadge'; - -export function AnchorProgramCard({ programId }: { programId: string }) { - const { url } = useCluster(); - const { idl } = useAnchorProgram(programId, url); - const [collapsedValue, setCollapsedValue] = useState(1); - - if (!idl) { - return null; - } - const spec = getIdlSpecType(idl); - - return ( -
    -
    -
    -
    -

    Anchor IDL

    -
    -
    - - Download IDL - -
    -
    -
    -
    - -
    - setCollapsedValue(e.target.checked ? false : 1)} - /> - -
    -
    - -
    - -
    -
    - ); -} diff --git a/app/components/account/IdlCard.tsx b/app/components/account/IdlCard.tsx index 88bad3008..99c416955 100644 --- a/app/components/account/IdlCard.tsx +++ b/app/components/account/IdlCard.tsx @@ -4,15 +4,18 @@ import { useCluster } from '@providers/cluster'; import { useState } from 'react'; import ReactJson from 'react-json-view'; -import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; import { getIdlSpecType } from '@/app/utils/convertLegacyIdl'; import { DownloadableButton } from '../common/Downloadable'; import { IDLBadge } from '../common/IDLBadge'; -export function IdlCard({ programId }: { programId: string }) { - const { url } = useCluster(); - const { idl } = useIdlFromProgramMetadataProgram(programId, url); +interface Props { + idl: Idl; + programId: string; + title?: string; +} + +export function IdlCard({ idl, programId, title = "Program IDL" }: Props) { const [collapsedValue, setCollapsedValue] = useState(1); if (!idl) { @@ -26,7 +29,7 @@ export function IdlCard({ programId }: { programId: string }) {
    -

    Program IDL (from Program Metadata PDA)

    +

    {title}

    Date: Tue, 17 Dec 2024 14:00:08 +0100 Subject: [PATCH 12/18] Update pnpm-lock.yaml --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b8e96bb1..c48cda956 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: specifier: ^4.3.1 version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) solana-program-metadata: - specifier: ^0.0.15 - version: 0.0.15(bufferutil@4.0.7)(utf-8-validate@5.0.10) + specifier: 1.0.0 + version: 1.0.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) superstruct: specifier: ^0.15.3 version: 0.15.3 @@ -4534,8 +4534,8 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - solana-program-metadata@0.0.15: - resolution: {integrity: sha512-EHFssAgLWcEgJNTxq6lFLmq2fH129lEjRLv9srin0biEqAGh+kDGygC2IiqIs0c9mJxZTfCrqSvhlRHB9zXsFA==} + solana-program-metadata@1.0.0: + resolution: {integrity: sha512-5YLcqGS9HpVpOudnNk2o6UPkrUIqddxhWWqqjTyZt5tAZPqgH4zq1pQ8ZNr1HOykeqOhqz7sbcVJisTkvknd4w==} hasBin: true source-map-js@1.0.2: @@ -11212,7 +11212,7 @@ snapshots: dot-case: 3.0.4 tslib: 2.5.0 - solana-program-metadata@0.0.15(bufferutil@4.0.7)(utf-8-validate@5.0.10): + solana-program-metadata@1.0.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): dependencies: '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) From 5ceaef35182210331343bb1b4d93a1f818434d1a Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 17 Dec 2024 14:07:32 +0100 Subject: [PATCH 13/18] Fix import --- app/components/account/IdlCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/account/IdlCard.tsx b/app/components/account/IdlCard.tsx index 99c416955..5a91d3d5b 100644 --- a/app/components/account/IdlCard.tsx +++ b/app/components/account/IdlCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCluster } from '@providers/cluster'; +import { Idl } from '@coral-xyz/anchor'; import { useState } from 'react'; import ReactJson from 'react-json-view'; From f8483b6b70d280e66900578dc697b9ac60f8a442 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 17 Dec 2024 16:12:01 +0100 Subject: [PATCH 14/18] Fix metadata length offset --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8f3e3636f..eb4d5fdea 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@solana/web3.js": "^1.66.6", "@solflare-wallet/utl-sdk": "^1.4.0", "@types/bn.js": "5.1.0", - "solana-program-metadata": "1.0.0", + "solana-program-metadata": "^1.0.3", "axios": "^0.28.0", "bignumber.js": "^9.0.2", "bn.js": "5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c48cda956..ef9f6ceff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: specifier: ^4.3.1 version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) solana-program-metadata: - specifier: 1.0.0 - version: 1.0.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) + specifier: ^1.0.3 + version: 1.0.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) superstruct: specifier: ^0.15.3 version: 0.15.3 @@ -4534,8 +4534,8 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - solana-program-metadata@1.0.0: - resolution: {integrity: sha512-5YLcqGS9HpVpOudnNk2o6UPkrUIqddxhWWqqjTyZt5tAZPqgH4zq1pQ8ZNr1HOykeqOhqz7sbcVJisTkvknd4w==} + solana-program-metadata@1.0.3: + resolution: {integrity: sha512-MC2Mf9vmKbaYtxblVgOPiwyN5bxJHZe/QPGGdZtnd1ZZ1KjQdo1GFeLQPkcFiVixs0hFWQIKt8tpEpNvI8p/kA==} hasBin: true source-map-js@1.0.2: @@ -11212,7 +11212,7 @@ snapshots: dot-case: 3.0.4 tslib: 2.5.0 - solana-program-metadata@1.0.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): + solana-program-metadata@1.0.3(bufferutil@4.0.7)(utf-8-validate@5.0.10): dependencies: '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) From b7e5104df55c12203293f79394f769198f547a96 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Sat, 21 Dec 2024 12:57:20 +0100 Subject: [PATCH 15/18] Update to newest program metadata version --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index eb4d5fdea..a6300fb36 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@solana/web3.js": "^1.66.6", "@solflare-wallet/utl-sdk": "^1.4.0", "@types/bn.js": "5.1.0", - "solana-program-metadata": "^1.0.3", + "solana-program-metadata": "^1.2.1", "axios": "^0.28.0", "bignumber.js": "^9.0.2", "bn.js": "5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef9f6ceff..ba01be6f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,8 +155,8 @@ importers: specifier: ^4.3.1 version: 4.3.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) solana-program-metadata: - specifier: ^1.0.3 - version: 1.0.3(bufferutil@4.0.7)(utf-8-validate@5.0.10) + specifier: ^1.2.1 + version: 1.2.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) superstruct: specifier: ^0.15.3 version: 0.15.3 @@ -4534,8 +4534,8 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - solana-program-metadata@1.0.3: - resolution: {integrity: sha512-MC2Mf9vmKbaYtxblVgOPiwyN5bxJHZe/QPGGdZtnd1ZZ1KjQdo1GFeLQPkcFiVixs0hFWQIKt8tpEpNvI8p/kA==} + solana-program-metadata@1.2.1: + resolution: {integrity: sha512-zbJfUNn12ZL1MhD+4pY9zLjlQB1wQDJTxABTQ4WEMq72QS+rfJuFI4nvrmtvmX2Z5wRlQDYH20vgSn2liiwVPg==} hasBin: true source-map-js@1.0.2: @@ -11212,7 +11212,7 @@ snapshots: dot-case: 3.0.4 tslib: 2.5.0 - solana-program-metadata@1.0.3(bufferutil@4.0.7)(utf-8-validate@5.0.10): + solana-program-metadata@1.2.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): dependencies: '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) '@solana/web3.js': 1.95.8(bufferutil@4.0.7)(utf-8-validate@5.0.10) From 02b8535dfd705a44a30aa1332a023425bb02778f Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Mon, 6 Jan 2025 17:20:05 -0500 Subject: [PATCH 16/18] redirect /anchor-program to /idl --- app/address/[address]/anchor-program/page.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/address/[address]/anchor-program/page.tsx diff --git a/app/address/[address]/anchor-program/page.tsx b/app/address/[address]/anchor-program/page.tsx new file mode 100644 index 000000000..e20baddc8 --- /dev/null +++ b/app/address/[address]/anchor-program/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from 'next/navigation'; + +type Props = Readonly<{ + params: { + address: string; + }; +}>; + +// Redirect to the new IDL page +export default function AnchorProgramIDLPage({ params: { address } }: Props) { + redirect(`/address/${address}/idl`); +} From 1ae7abe447556555fbe73a46f581df7e7709ebc2 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Tue, 7 Jan 2025 12:19:14 -0500 Subject: [PATCH 17/18] clean up program metadata idl fetching hooks --- app/address/[address]/idl/page.tsx | 42 +++++--- app/address/[address]/layout.tsx | 7 +- app/components/account/IdlCard.tsx | 14 ++- app/providers/anchor.tsx | 131 ++++++------------------- app/providers/idl.tsx | 149 +++++++++++++++-------------- app/providers/program-metadata.tsx | 17 +++- 6 files changed, 162 insertions(+), 198 deletions(-) diff --git a/app/address/[address]/idl/page.tsx b/app/address/[address]/idl/page.tsx index 9054f5747..f5972d2e4 100644 --- a/app/address/[address]/idl/page.tsx +++ b/app/address/[address]/idl/page.tsx @@ -1,16 +1,24 @@ 'use client'; import { IdlCard } from '@components/account/IdlCard'; -import { useAnchorProgram } from '@providers/anchor'; +import { useIdlFromAnchorProgramSeed } from '@providers/anchor'; import { useCluster } from '@providers/cluster'; -import { useIdlFromProgramMetadataProgram } from '@providers/idl'; -import { useState } from 'react'; +import { useIdlFromMetadataProgram } from '@providers/idl'; +import { Suspense, useEffect, useState } from 'react'; export default function IdlPage({ params: { address } }: { params: { address: string } }) { const { url } = useCluster(); + const anchorIdl = useIdlFromAnchorProgramSeed(address, url, false); + const metadataIdl = useIdlFromMetadataProgram(address, url, false); + const [activeTab, setActiveTab] = useState<'anchor' | 'metadata'>('anchor'); - const { idl: anchorIdl } = useAnchorProgram(address, url); - const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(address, url); + + useEffect(() => { + // Show whatever tab is available + if (!anchorIdl && metadataIdl) { + setActiveTab('metadata'); + } + }, [anchorIdl, metadataIdl]); return (
    @@ -39,13 +47,25 @@ export default function IdlPage({ params: { address } }: { params: { address: st
    - {activeTab === 'anchor' && anchorIdl && ( - - )} - {activeTab === 'metadata' && metadataIdl && ( - - )} + Loading...
    }> + {activeTab === 'anchor' && anchorIdl && } + {activeTab === 'metadata' && metadataIdl && ( + + )} +
    ); } + +function ProgramMetadataIdlCard({ programId, url }: { programId: string; url: string }) { + const idl = useIdlFromMetadataProgram(programId, url, true); + + return ; +} + +function AnchorIdlCard({ programId, url }: { programId: string; url: string }) { + const idl = useIdlFromAnchorProgramSeed(programId, url, true); + + return ; +} diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index f25b93dbc..6a6d62225 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -50,7 +50,6 @@ import { Address } from 'web3js-experimental'; import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard'; import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft'; -import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl'; import { useProgramMetadata } from '@/app/providers/program-metadata'; import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig'; import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info'; @@ -790,13 +789,13 @@ function ProgramMetaDataLink({ tab, address, pubkey }: { tab: Tab; address: stri function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) { const { url } = useCluster(); - const { idl: anchorIdl } = useAnchorProgram(pubkey.toString(), url); - const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(pubkey.toString(), url); + const { idl } = useAnchorProgram(pubkey.toString(), url); const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` }); const selectedLayoutSegment = useSelectedLayoutSegment(); const isActive = selectedLayoutSegment === tab.path; - if (!anchorIdl && !metadataIdl) { + // Will be null if no anchor and no program metadata IDL + if (!idl) { return null; } diff --git a/app/components/account/IdlCard.tsx b/app/components/account/IdlCard.tsx index 5a91d3d5b..1db8ecf23 100644 --- a/app/components/account/IdlCard.tsx +++ b/app/components/account/IdlCard.tsx @@ -1,13 +1,17 @@ -'use client'; - import { Idl } from '@coral-xyz/anchor'; import { useState } from 'react'; -import ReactJson from 'react-json-view'; import { getIdlSpecType } from '@/app/utils/convertLegacyIdl'; import { DownloadableButton } from '../common/Downloadable'; import { IDLBadge } from '../common/IDLBadge'; +import dynamic from 'next/dynamic'; + +// Necessary to avoid hydration errors +const ReactJson = dynamic(() => import('react-json-view'), { + ssr: false, + loading: () =>
    Loading IDL...
    , +}); interface Props { idl: Idl; @@ -15,13 +19,13 @@ interface Props { title?: string; } -export function IdlCard({ idl, programId, title = "Program IDL" }: Props) { +export function IdlCard({ idl, programId, title = 'Program IDL' }: Props) { const [collapsedValue, setCollapsedValue] = useState(1); if (!idl) { - console.log('No IDL found'); return null; } + const spec = getIdlSpecType(idl); return ( diff --git a/app/providers/anchor.tsx b/app/providers/anchor.tsx index 45d10512c..35638dee8 100644 --- a/app/providers/anchor.tsx +++ b/app/providers/anchor.tsx @@ -1,130 +1,55 @@ import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; -import * as elfy from 'elfy'; -import pako from 'pako'; -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; +import useSWR from 'swr'; import { formatIdl } from '../utils/convertLegacyIdl'; -import { useAccountInfo, useFetchAccountInfo } from './accounts'; -import { useIdlFromProgramMetadataProgram } from './idl'; - -const cachedAnchorProgramPromises: Record< - string, - void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: Idl | null } -> = {}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function useIdlFromSolanaProgramBinary(programAddress: string): Idl | null { - const fetchAccountInfo = useFetchAccountInfo(); - const programInfo = useAccountInfo(programAddress); - const programDataAddress: string | undefined = programInfo?.data?.data.parsed?.parsed.info['programData']; - const programDataInfo = useAccountInfo(programDataAddress); - - useEffect(() => { - if (!programInfo) { - fetchAccountInfo(new PublicKey(programAddress), 'parsed'); - } - }, [programAddress, fetchAccountInfo, programInfo]); - - useEffect(() => { - if (programDataAddress && !programDataInfo) { - fetchAccountInfo(new PublicKey(programDataAddress), 'raw'); - } - }, [programDataAddress, fetchAccountInfo, programDataInfo]); - - const param = useMemo(() => { - if (programDataInfo && programDataInfo.data && programDataInfo.data.data.raw) { - const offset = - (programInfo?.data?.owner.toString() ?? '') === 'BPFLoaderUpgradeab1e11111111111111111111111' ? 45 : 0; - const raw = Buffer.from(programDataInfo.data.data.raw.slice(offset)); - - try { - return parseIdlFromElf(raw); - } catch (e) { - return null; - } - } - return null; - }, [programDataInfo, programInfo]); - return param; -} - -function parseIdlFromElf(elfBuffer: any) { - const elf = elfy.parse(elfBuffer); - const solanaIdlSection = elf.body.sections.find((section: any) => section.name === '.solana.idl'); - if (!solanaIdlSection) { - throw new Error('.solana.idl section not found'); - } - - // Extract the section data - const solanaIdlData = solanaIdlSection.data; - - // Parse the section data - solanaIdlData.readUInt32LE(4); - const ptr = solanaIdlData.readUInt32LE(4); - const size = solanaIdlData.readBigUInt64LE(8); - - // Get the compressed bytes - const byteRange = elfBuffer.slice(ptr, ptr + Number(size)); - - // Decompress the IDL - try { - const inflatedIdl = JSON.parse(new TextDecoder().decode(pako.inflate(byteRange))); - return inflatedIdl; - } catch (err) { - console.error('Failed to decompress data:', err); - return null; - } -} +import { useIdlFromMetadataProgram } from './idl'; function getProvider(url: string) { return new AnchorProvider(new Connection(url), new NodeWallet(Keypair.generate()), {}); } -function useIdlFromAnchorProgramSeed(programAddress: string, url: string): Idl | null { - const key = `${programAddress}-${url}`; - const cacheEntry = cachedAnchorProgramPromises[key]; +export function useIdlFromAnchorProgramSeed(programAddress: string, url: string, useSuspense = true): Idl | null { + const { data: idl } = useSWR( + [`anchor-idl`, programAddress, url], + async () => { + try { + const programId = new PublicKey(programAddress); + const idl = await Program.fetchIdl(programId, getProvider(url)); - if (cacheEntry === undefined) { - const programId = new PublicKey(programAddress); - const promise = Program.fetchIdl(programId, getProvider(url)) - .then(idl => { if (!idl) { - throw new Error(`IDL not found for program: ${programAddress.toString()}`); + throw new Error(`Anchor IDL not found for program: ${programAddress}`); } - cachedAnchorProgramPromises[key] = { - __type: 'result', - result: idl, - }; - }) - .catch(_ => { - cachedAnchorProgramPromises[key] = { __type: 'result', result: null }; - }); - cachedAnchorProgramPromises[key] = { - __type: 'promise', - promise, - }; - throw promise; - } else if (cacheEntry.__type === 'promise') { - throw cacheEntry.promise; - } - return cacheEntry.result; + return idl; + } catch (error) { + return null; + } + }, + { + revalidateOnFocus: false, + suspense: useSuspense, + } + ); + + return idl ?? null; } export function useAnchorProgram( programAddress: string, url: string ): { program: Program | null; idl: Idl | null } { - const idlFromMetadataProgram = useIdlFromProgramMetadataProgram(programAddress, url); - const idlFromAnchorProgram = useIdlFromAnchorProgramSeed(programAddress, url); + // Can't use suspense here because it will early return & not obey rules of hooks + const idlFromAnchorProgram = useIdlFromAnchorProgramSeed(programAddress, url, false); + const idlFromMetadataProgram = useIdlFromMetadataProgram(programAddress, url, false); - // First try anchor program IDL, then fall back to metadata program IDL - const idl = idlFromAnchorProgram || idlFromMetadataProgram?.idl; + const idl = idlFromAnchorProgram || idlFromMetadataProgram; const program = useMemo(() => { if (!idl) return null; + try { return new Program(formatIdl(idl, programAddress), getProvider(url)); } catch (e) { diff --git a/app/providers/idl.tsx b/app/providers/idl.tsx index 3be12f5da..171712212 100644 --- a/app/providers/idl.tsx +++ b/app/providers/idl.tsx @@ -1,86 +1,95 @@ -import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; -import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'; -import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { Idl } from '@coral-xyz/anchor'; +import { Connection, PublicKey } from '@solana/web3.js'; import { useMemo } from 'react'; -import { fetchIDL } from 'solana-program-metadata'; +import * as elfy from 'elfy'; +import { useAccountInfo, useFetchAccountInfo } from './accounts'; +import pako from 'pako'; +import useSWR from 'swr'; +import { useEffect } from 'react'; -import { formatIdl } from '../utils/convertLegacyIdl'; +import { fetchIdlFromMetadataProgram } from './program-metadata'; -const cachedIdlPromises: Record< - string, - void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: Idl | null } -> = {}; +export function useIdlFromMetadataProgram(programAddress: string, url: string, useSuspense = true): Idl | null { + const { data: idl } = useSWR( + [`program-metadata-idl`, programAddress, url], + async () => { + try { + const connection = new Connection(url); + return await fetchIdlFromMetadataProgram(programAddress, connection); + } catch (error) { + return null; + } + }, + { + revalidateOnFocus: false, + suspense: useSuspense, + } + ); -async function fetchIdlFromMetadataProgram(programAddress: string, connection: Connection): Promise { - const result = await fetchIDL(new PublicKey(programAddress), connection.rpcEndpoint); + return idl ?? null; +} - if (!result) { - console.error('IDL not found!'); - return null; +function parseIdlFromElf(elfBuffer: any) { + const elf = elfy.parse(elfBuffer); + const solanaIdlSection = elf.body.sections.find((section: any) => section.name === '.solana.idl'); + if (!solanaIdlSection) { + throw new Error('.solana.idl section not found'); } - const idl = JSON.parse(result); - return idl; -} + // Extract the section data + const solanaIdlData = solanaIdlSection.data; -function useIdlFromMetadataProgram(programAddress: string, url: string): Idl | null { - const key = `${programAddress}-${url}`; - const cacheEntry = cachedIdlPromises[key]; + // Parse the section data + solanaIdlData.readUInt32LE(4); + const ptr = solanaIdlData.readUInt32LE(4); + const size = solanaIdlData.readBigUInt64LE(8); - // If there's no cached entry, start fetching the IDL - if (cacheEntry === undefined) { - const connection = new Connection(url); - const promise = fetchIdlFromMetadataProgram(programAddress, connection) - .then(idl => { - cachedIdlPromises[key] = { - __type: 'result', - result: idl, - }; - }) - .catch(err => { - console.error('Error fetching IDL:', err); - cachedIdlPromises[key] = { __type: 'result', result: null }; - }); - cachedIdlPromises[key] = { - __type: 'promise', - promise, - }; - throw promise; // Throw the promise for React Suspense - } + // Get the compressed bytes + const byteRange = elfBuffer.slice(ptr, ptr + Number(size)); - // If the cache has a pending promise, throw it - if (cacheEntry.__type === 'promise') { - throw cacheEntry.promise; + // Decompress the IDL + try { + const inflatedIdl = JSON.parse(new TextDecoder().decode(pako.inflate(byteRange))); + return inflatedIdl; + } catch (err) { + console.error('Failed to decompress data:', err); + return null; } - - // Return the cached result - return cacheEntry.result; } -function getProvider(url: string) { - return new AnchorProvider(new Connection(url), new NodeWallet(Keypair.generate()), {}); -} +// Unused +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function useIdlFromSolanaProgramBinary(programAddress: string): Idl | null { + const fetchAccountInfo = useFetchAccountInfo(); + const programInfo = useAccountInfo(programAddress); + const programDataAddress: string | undefined = programInfo?.data?.data.parsed?.parsed.info['programData']; + const programDataInfo = useAccountInfo(programDataAddress); -export function useIdlFromProgramMetadataProgram( - programAddress: string, - url: string -): { program: Program | null; idl: Idl | null } { - const idlFromProgramMetadataProgram = useIdlFromMetadataProgram(programAddress, url); - const idl = idlFromProgramMetadataProgram; - const program: Program | null = useMemo(() => { - if (!idl) return null; - try { - const program = new Program(formatIdl(idl, programAddress), getProvider(url)); - return program; - } catch (e) { - console.error('Error creating anchor program for', programAddress, e, { idl }); - return null; + useEffect(() => { + if (!programInfo) { + fetchAccountInfo(new PublicKey(programAddress), 'parsed'); } - }, [idl, programAddress, url]); - return { idl, program }; -} + }, [programAddress, fetchAccountInfo, programInfo]); + + useEffect(() => { + if (programDataAddress && !programDataInfo) { + fetchAccountInfo(new PublicKey(programDataAddress), 'raw'); + } + }, [programDataAddress, fetchAccountInfo, programDataInfo]); + + const param = useMemo(() => { + if (programDataInfo && programDataInfo.data && programDataInfo.data.data.raw) { + const offset = + (programInfo?.data?.owner.toString() ?? '') === 'BPFLoaderUpgradeab1e11111111111111111111111' ? 45 : 0; + const raw = Buffer.from(programDataInfo.data.data.raw.slice(offset)); -export type AnchorAccount = { - layout: string; - account: object; -}; + try { + return parseIdlFromElf(raw); + } catch (e) { + return null; + } + } + return null; + }, [programDataInfo, programInfo]); + return param; +} diff --git a/app/providers/program-metadata.tsx b/app/providers/program-metadata.tsx index 14bc3e629..1c13b2553 100644 --- a/app/providers/program-metadata.tsx +++ b/app/providers/program-metadata.tsx @@ -1,5 +1,6 @@ +import { Idl } from '@coral-xyz/anchor'; import { Connection, PublicKey } from '@solana/web3.js'; -import { fetchProgramMetadata, ProgramMetaData } from 'solana-program-metadata'; +import { fetchIDL, fetchProgramMetadata, ProgramMetaData } from 'solana-program-metadata'; const cachedLogoProgramPromises: Record< string, @@ -55,7 +56,13 @@ export function useProgramMetadata(programAddress: string, url: string): Program return programMetaData; } -export type AnchorAccount = { - layout: string; - account: object; -}; +export async function fetchIdlFromMetadataProgram(programAddress: string, connection: Connection): Promise { + const result = await fetchIDL(new PublicKey(programAddress), connection.rpcEndpoint); + + if (!result) { + return null; + } + + const idl = JSON.parse(result); + return idl; +} From e2eb5995119979d9d3ac8eaf3be9e74ad9255de0 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Tue, 7 Jan 2025 13:14:56 -0500 Subject: [PATCH 18/18] fix lint --- app/components/account/IdlCard.tsx | 4 ++-- app/providers/idl.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/account/IdlCard.tsx b/app/components/account/IdlCard.tsx index 1db8ecf23..a7961b522 100644 --- a/app/components/account/IdlCard.tsx +++ b/app/components/account/IdlCard.tsx @@ -1,16 +1,16 @@ import { Idl } from '@coral-xyz/anchor'; +import dynamic from 'next/dynamic'; import { useState } from 'react'; import { getIdlSpecType } from '@/app/utils/convertLegacyIdl'; import { DownloadableButton } from '../common/Downloadable'; import { IDLBadge } from '../common/IDLBadge'; -import dynamic from 'next/dynamic'; // Necessary to avoid hydration errors const ReactJson = dynamic(() => import('react-json-view'), { - ssr: false, loading: () =>
    Loading IDL...
    , + ssr: false, }); interface Props { diff --git a/app/providers/idl.tsx b/app/providers/idl.tsx index 171712212..cb81ed4d8 100644 --- a/app/providers/idl.tsx +++ b/app/providers/idl.tsx @@ -1,12 +1,12 @@ import { Idl } from '@coral-xyz/anchor'; import { Connection, PublicKey } from '@solana/web3.js'; -import { useMemo } from 'react'; import * as elfy from 'elfy'; -import { useAccountInfo, useFetchAccountInfo } from './accounts'; import pako from 'pako'; -import useSWR from 'swr'; +import { useMemo } from 'react'; import { useEffect } from 'react'; +import useSWR from 'swr'; +import { useAccountInfo, useFetchAccountInfo } from './accounts'; import { fetchIdlFromMetadataProgram } from './program-metadata'; export function useIdlFromMetadataProgram(programAddress: string, url: string, useSuspense = true): Idl | null {