From 74bc5b397ecc418d35d3371f5bcf9314a01a9df9 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Mon, 29 Jan 2024 13:13:39 -0500 Subject: [PATCH] make t22 nfts image show up --- app/address/[address]/layout.tsx | 14 ++- app/components/Token22MetadataHeader.tsx | 84 ++++++++++++++ .../account/SplTokenMetadataInterfaceCard.tsx | 107 ++++++++++-------- app/providers/accounts/index.tsx | 16 ++- app/providers/accounts/metadata-extension.tsx | 44 +++++++ app/providers/accounts/utils/isT22NFT.ts | 10 ++ 6 files changed, 217 insertions(+), 58 deletions(-) create mode 100644 app/components/Token22MetadataHeader.tsx create mode 100644 app/providers/accounts/metadata-extension.tsx create mode 100644 app/providers/accounts/utils/isT22NFT.ts diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx index 5694eafb..0b67d0e1 100644 --- a/app/address/[address]/layout.tsx +++ b/app/address/[address]/layout.tsx @@ -52,6 +52,9 @@ import { WalletProvider } from '@solana/wallet-adapter-react'; import { ConnectionProvider } from '@solana/wallet-adapter-react'; import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { Token22NFTHeader } from '@/app/components/Token22MetadataHeader'; +import isT22NFT from '@/app/providers/accounts/utils/isT22NFT'; + function WalletAdapterProviders({ children }: { children: React.ReactNode }) { const { url } = useCluster(); @@ -295,6 +298,10 @@ function AccountHeader({ return ; } + if (isT22NFT(parsedData)) { + return ; + } + const nftokenNFT = account && isNFTokenAccount(account); if (nftokenNFT && account) { return ; @@ -534,12 +541,7 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] { // Add SPL Token Metadata Interface tab console.log('Parsed data', parsedData); - if ( - parsedData && - parsedData.parsed.info && - parsedData.parsed.info.extensions && - (parsedData.parsed.info.extensions as Record[]).find(ext => ext.extension === 'metadataPointer') - ) { + if (isT22NFT(parsedData)) { tabs.push(TABS_LOOKUP['spl-token-metadata-interface'][0]); } diff --git a/app/components/Token22MetadataHeader.tsx b/app/components/Token22MetadataHeader.tsx new file mode 100644 index 00000000..70075aa7 --- /dev/null +++ b/app/components/Token22MetadataHeader.tsx @@ -0,0 +1,84 @@ +import { ArtContent } from '@components/common/NFTArt'; +import React, { useMemo } from 'react'; +import useAsyncEffect from 'use-async-effect'; + +import { LoadingState as TokenMetadataLoadingState, useTokenMetadata } from './account/SplTokenMetadataInterfaceCard'; + +enum LoadingState { + PreconditionFailed, + Started, + Succeeded, + Failed, +} + +function useTokenMetadataWithUri(mint: string) { + const { loading, metadata } = useTokenMetadata(mint); + + const initialLoadingState = + metadata && loading === TokenMetadataLoadingState.MetadataFound + ? LoadingState.Started + : LoadingState.PreconditionFailed; + const [jsonLoading, setJsonLoading] = React.useState(initialLoadingState); + const [metadataJson, setMetadataJson] = React.useState(null); + + useAsyncEffect(async () => { + if (!metadata) { + return; + } + try { + const result = await fetch(metadata.uri); + if (result.ok) { + const json = await result.json(); + setMetadataJson(json); + setJsonLoading(LoadingState.Succeeded); + } else { + setJsonLoading(LoadingState.Failed); + } + } catch (err) { + setJsonLoading(LoadingState.Failed); + } + }, [loading, metadata]); + + return useMemo(() => ({ jsonLoading, jsonMetadata: metadataJson }), [jsonLoading, metadataJson]); +} + +export function Token22NFTHeader({ mint }: { mint: string }) { + const { metadata } = useTokenMetadata(mint); + const { jsonLoading, jsonMetadata } = useTokenMetadataWithUri(mint); + + const ui = useMemo(() => { + if (jsonLoading === LoadingState.PreconditionFailed || jsonLoading === LoadingState.Started) { + return
Loading...
; + } + + if (jsonLoading === LoadingState.Failed || !jsonMetadata || !metadata) { + return
Failed
; + } + + return ( +
+
+ +
+
+ {
Token Extension NFT
} +
+

+ {metadata.name !== '' ? metadata.name : 'No NFT name was found'} +

+
+

+ {metadata.symbol !== '' ? metadata.symbol : 'No Symbol was found'} +

+
{new Map(metadata.additionalMetadata).get('Description')}
+
+
+ ); + }, [metadata, jsonLoading, jsonMetadata, mint]); + + return ui; +} + +// function getIsMutablePill(isMutable: boolean) { +// return {`${isMutable ? 'Mutable' : 'Immutable'}`}; +// } diff --git a/app/components/account/SplTokenMetadataInterfaceCard.tsx b/app/components/account/SplTokenMetadataInterfaceCard.tsx index d44c6f3e..798f0bd6 100644 --- a/app/components/account/SplTokenMetadataInterfaceCard.tsx +++ b/app/components/account/SplTokenMetadataInterfaceCard.tsx @@ -1,15 +1,15 @@ import { base64 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { createEmitInstruction, TokenMetadata, unpack as deserializeTokenMetadata } from '@solana/spl-token-metadata'; import { useConnection } from '@solana/wallet-adapter-react'; -import { MessageV0, PublicKey, VersionedTransaction } from '@solana/web3.js'; -import { useEffect, useMemo, useState } from 'react'; +import { Connection, MessageV0, PublicKey, VersionedTransaction } from '@solana/web3.js'; +import { useMemo, useState } from 'react'; import useAsyncEffect from 'use-async-effect'; import { useMintAccountInfo } from '@/app/providers/accounts'; import { Address } from '../common/Address'; -enum LoadingState { +export enum LoadingState { Idle, Loading, MintMissing, @@ -55,7 +55,7 @@ function SplTokenMetadata({ metadata }: { metadata: TokenMetadata }) { ); } -async function getTokenMetadata(connection: any, programId: PublicKey, metadataPointer: PublicKey) { +async function getTokenMetadata(connection: Connection, programId: PublicKey, metadataPointer: PublicKey) { const ix = createEmitInstruction({ metadata: metadataPointer, programId }); const message = MessageV0.compile({ instructions: [ix], @@ -70,80 +70,83 @@ async function getTokenMetadata(connection: any, programId: PublicKey, metadataP replaceRecentBlockhash: true, sigVerify: false, }); - console.log('Simul result:', result); + if (result.value.returnData) { - console.log(result.value.returnData); const buffer = base64.decode(result.value.returnData.data[0]); - console.log(buffer.length); return deserializeTokenMetadata(buffer); } return null; } -export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) { +export function useTokenMetadata(mint: string) { const { connection } = useConnection(); - const [loading, setLoading] = useState(LoadingState.Idle); - const [metadata, setMetadata] = useState(null); - const [metadataAuthority, setMetadataAuthority] = useState(null); - const [metadataPointer, setMetadataPointer] = useState(null); - const mintInfo = useMintAccountInfo(mint); - const [extensions, setExtensions] = useState[] | null>(null); - useEffect(() => { - console.log('mintInfo:', mintInfo); - if (!mintInfo || !(mintInfo as any).extensions) { - setLoading(LoadingState.MintMissing); - } + let initialLoadingState = LoadingState.Idle; + if (!mintInfo || !(mintInfo as any).extensions) { + initialLoadingState = LoadingState.MintMissing; + } + const extensions = (mintInfo as any).extensions; + const metadataPointerExt = extensions.find((ext: any) => ext.extension === 'metadataPointer'); - const extensions = (mintInfo as any).extensions; - setExtensions(extensions); - const metadataPointerExt = extensions.find((ext: any) => ext.extension === 'metadataPointer'); - if (!metadataPointerExt) { - setLoading(LoadingState.MetadataExtensionMissing); - } else { - setMetadataPointer(metadataPointerExt.state.metadataAddress); - setMetadataAuthority(metadataPointerExt.state.authority); - } - }, [mintInfo]); + if (!metadataPointerExt) { + initialLoadingState = LoadingState.MetadataExtensionMissing; + } + const metadataPointer = metadataPointerExt.state.metadataAddress; + const metadataAuthority = metadataPointerExt.state.authority; - // eslint-disable-next-line react-hooks/rules-of-hooks - useAsyncEffect(async () => { - if (extensions === null || metadataPointer === null) { - console.log('F'); - setLoading(LoadingState.MetadataExtensionMissing); - return; + const [loading, setLoading] = useState(initialLoadingState); + const [metadata, setMetadata] = useState(null); + const [programOwner, setProgramOwner] = useState(null); + + // Use cached data from the mint account if possible + if (metadataPointer === mint) { + const tokenMetadataExt = (mintInfo as any).extensions.find((ext: any) => ext.extension === 'tokenMetadata'); + if (tokenMetadataExt) { + setLoading(LoadingState.MetadataFound); + const mintMetadata: TokenMetadata = tokenMetadataExt.state; + setMetadata(mintMetadata); } + setLoading(LoadingState.MetadataExtensionMissing); + } - // Use cached data from the mint account if possible - if (metadataPointer === mint) { - const tokenMetadataExt = extensions.find((ext: any) => ext.extension === 'tokenMetadata'); - if (tokenMetadataExt) { - setLoading(LoadingState.MetadataFound); - const mintMetadata: TokenMetadata = tokenMetadataExt.state; - setMetadata(mintMetadata); - return; - } - setLoading(LoadingState.MetadataExtensionMissing); + useAsyncEffect(async () => { + if (metadata) { return; } setLoading(LoadingState.Loading); + const metadataAccountInfo = await connection.getAccountInfo(new PublicKey(metadataPointer)); if (!metadataAccountInfo) { setLoading(LoadingState.MetadataAccountMissing); return; } - const metadata = await getTokenMetadata(connection, metadataAccountInfo.owner, new PublicKey(metadataPointer)); - if (metadata) { - setMetadata(metadata); + setProgramOwner(metadataAccountInfo.owner); + const tokenMetadata = await getTokenMetadata( + connection, + metadataAccountInfo.owner, + new PublicKey(metadataPointer) + ); + + if (tokenMetadata) { + setMetadata(tokenMetadata); setLoading(LoadingState.MetadataFound); } else { setLoading(LoadingState.MetadataAccountMissing); } - }, [mint, connection, mintInfo, metadataPointer]); + }, [connection, metadataPointer, metadata]); + + return useMemo( + () => ({ loading, metadata, metadataAuthority, metadataPointer, programOwner }), + [loading, metadata, metadataAuthority, metadataPointer, programOwner] + ); +} + +export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) { + const { loading, metadata, metadataAuthority, metadataPointer, programOwner } = useTokenMetadata(mint); const metadataCard = useMemo(() => { return ( @@ -177,6 +180,10 @@ export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) { + + Program + {programOwner ?
: 'Missing'} + Metadata Address @@ -203,7 +210,7 @@ export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) { ); - }, [loading, metadata, metadataAuthority, metadataPointer, mint]); + }, [metadataAuthority, metadataPointer, metadataCard, programOwner]); return card; } diff --git a/app/providers/accounts/index.tsx b/app/providers/accounts/index.tsx index 4fa4e639..9da2abd5 100644 --- a/app/providers/accounts/index.tsx +++ b/app/providers/accounts/index.tsx @@ -62,7 +62,7 @@ export function isTokenProgramData(data: { program: string }): data is TokenProg try { assertIsTokenProgram(data.program); return true; - } catch(e) { + } catch (e) { return false; } } @@ -97,6 +97,17 @@ export type AddressLookupTableProgramData = { parsed: ParsedAddressLookupTableAccount; }; +type ExtensionRecord = Record & { extension: string }; +export type TokenMintExtensionData = { + program: 'spl-token-2022'; + parsed: { + type: 'mint'; + info: { + extensions: ExtensionRecord[]; + }; + }; +}; + export type ParsedData = | UpgradeableLoaderAccountData | StakeProgramData @@ -105,7 +116,8 @@ export type ParsedData = | NonceProgramData | SysvarProgramData | ConfigProgramData - | AddressLookupTableProgramData; + | AddressLookupTableProgramData + | TokenMintExtensionData; export interface AccountData { parsed?: ParsedData; diff --git a/app/providers/accounts/metadata-extension.tsx b/app/providers/accounts/metadata-extension.tsx new file mode 100644 index 00000000..5304a87e --- /dev/null +++ b/app/providers/accounts/metadata-extension.tsx @@ -0,0 +1,44 @@ +import { base64 } from '@project-serum/anchor/dist/cjs/utils/bytes'; +import { createEmitInstruction, TokenMetadata, unpack as deserializeTokenMetadata } from '@solana/spl-token-metadata'; +import { useConnection } from '@solana/wallet-adapter-react'; +import { MessageV0, PublicKey, VersionedTransaction } from '@solana/web3.js'; +import { useEffect, useState } from 'react'; + +export function useTokenMetadataExtension(programId: PublicKey | undefined, metadataPointer: PublicKey | undefined) { + const { connection } = useConnection(); + const [tokenMetadata, setTokenMetadata] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchTokenMetadata() { + if (!programId || !metadataPointer) { + return; + } + const ix = createEmitInstruction({ metadata: metadataPointer, programId }); + const message = MessageV0.compile({ + instructions: [ix], + payerKey: new PublicKey('86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY'), + recentBlockhash: (await connection.getLatestBlockhashAndContext()).value.blockhash, + }); + + const tx = new VersionedTransaction(message); + const result = await connection.simulateTransaction(tx, { + commitment: 'confirmed', + replaceRecentBlockhash: true, + sigVerify: false, + }); + + if (result.value.returnData) { + const buffer = base64.decode(result.value.returnData.data[0]); + console.log('Inner found metadata, setting'); + setTokenMetadata(deserializeTokenMetadata(buffer)); + } + + setLoading(false); + } + + fetchTokenMetadata(); + }, [connection, programId, metadataPointer]); + + return loading ? null : tokenMetadata; +} diff --git a/app/providers/accounts/utils/isT22NFT.ts b/app/providers/accounts/utils/isT22NFT.ts new file mode 100644 index 00000000..80f667ee --- /dev/null +++ b/app/providers/accounts/utils/isT22NFT.ts @@ -0,0 +1,10 @@ +import { ParsedData, TokenMintExtensionData } from '..'; + +export default function isT22NFT(parsedData?: ParsedData): parsedData is TokenMintExtensionData { + return ( + parsedData && + parsedData.parsed.info && + parsedData.parsed.info.extensions && + (parsedData.parsed.info.extensions as Record[]).find(ext => ext.extension === 'metadataPointer') + ); +}