Skip to content

Commit

Permalink
make t22 nfts image show up
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra committed Jan 29, 2024
1 parent e6193a4 commit 74bc5b3
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 58 deletions.
14 changes: 8 additions & 6 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -295,6 +298,10 @@ function AccountHeader({
return <MetaplexNFTHeader nftData={parsedData.nftData} address={address} />;
}

if (isT22NFT(parsedData)) {
return <Token22NFTHeader mint={address} />;
}

const nftokenNFT = account && isNFTokenAccount(account);
if (nftokenNFT && account) {
return <NFTokenAccountHeader account={account} />;
Expand Down Expand Up @@ -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<string, string>[]).find(ext => ext.extension === 'metadataPointer')
) {
if (isT22NFT(parsedData)) {
tabs.push(TABS_LOOKUP['spl-token-metadata-interface'][0]);
}

Expand Down
84 changes: 84 additions & 0 deletions app/components/Token22MetadataHeader.tsx
Original file line number Diff line number Diff line change
@@ -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<LoadingState>(initialLoadingState);
const [metadataJson, setMetadataJson] = React.useState<object | null>(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 <div>Loading...</div>;
}

if (jsonLoading === LoadingState.Failed || !jsonMetadata || !metadata) {
return <div>Failed</div>;
}

return (
<div className="row">
<div className="col-auto ms-2 d-flex align-items-center">
<ArtContent pubkey={mint} data={jsonMetadata as any} />
</div>
<div className="col mb-3 ms-0.5 mt-3">
{<h6 className="header-pretitle ms-1">Token Extension NFT</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{metadata.name !== '' ? metadata.name : 'No NFT name was found'}
</h2>
</div>
<h4 className="header-pretitle ms-1 mt-1 no-overflow-with-ellipsis">
{metadata.symbol !== '' ? metadata.symbol : 'No Symbol was found'}
</h4>
<div className="ms-1">{new Map(metadata.additionalMetadata).get('Description')}</div>
</div>
</div>
);
}, [metadata, jsonLoading, jsonMetadata, mint]);

return ui;
}

// function getIsMutablePill(isMutable: boolean) {
// return <span className="badge badge-pill bg-dark">{`${isMutable ? 'Mutable' : 'Immutable'}`}</span>;
// }
107 changes: 57 additions & 50 deletions app/components/account/SplTokenMetadataInterfaceCard.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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],
Expand All @@ -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>(LoadingState.Idle);
const [metadata, setMetadata] = useState<TokenMetadata | null>(null);
const [metadataAuthority, setMetadataAuthority] = useState<string | null>(null);
const [metadataPointer, setMetadataPointer] = useState<string | null>(null);

const mintInfo = useMintAccountInfo(mint);
const [extensions, setExtensions] = useState<Record<string, any>[] | 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<LoadingState>(initialLoadingState);
const [metadata, setMetadata] = useState<TokenMetadata | null>(null);
const [programOwner, setProgramOwner] = useState<PublicKey | null>(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 (
Expand Down Expand Up @@ -177,6 +180,10 @@ export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) {
</tr>
</thead>
<tbody>
<tr>
<td>Program</td>
<td>{programOwner ? <Address pubkey={programOwner} link /> : 'Missing'}</td>
</tr>
<tr>
<td>Metadata Address</td>
<td>
Expand All @@ -203,7 +210,7 @@ export function SplTokenMetadataInterfaceCard({ mint }: { mint: string }) {
</div>
</div>
);
}, [loading, metadata, metadataAuthority, metadataPointer, mint]);
}, [metadataAuthority, metadataPointer, metadataCard, programOwner]);

return card;
}
16 changes: 14 additions & 2 deletions app/providers/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -97,6 +97,17 @@ export type AddressLookupTableProgramData = {
parsed: ParsedAddressLookupTableAccount;
};

type ExtensionRecord = Record<string, string> & { extension: string };
export type TokenMintExtensionData = {
program: 'spl-token-2022';
parsed: {
type: 'mint';
info: {
extensions: ExtensionRecord[];
};
};
};

export type ParsedData =
| UpgradeableLoaderAccountData
| StakeProgramData
Expand All @@ -105,7 +116,8 @@ export type ParsedData =
| NonceProgramData
| SysvarProgramData
| ConfigProgramData
| AddressLookupTableProgramData;
| AddressLookupTableProgramData
| TokenMintExtensionData;

export interface AccountData {
parsed?: ParsedData;
Expand Down
44 changes: 44 additions & 0 deletions app/providers/accounts/metadata-extension.tsx
Original file line number Diff line number Diff line change
@@ -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<TokenMetadata | null>(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;
}
10 changes: 10 additions & 0 deletions app/providers/accounts/utils/isT22NFT.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>[]).find(ext => ext.extension === 'metadataPointer')
);
}

0 comments on commit 74bc5b3

Please sign in to comment.