Skip to content

Commit

Permalink
add spl-token-metadata-interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra committed Jan 17, 2024
1 parent 8633a39 commit 84e7948
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 1 deletion.
20 changes: 19 additions & 1 deletion app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ const TABS_LOOKUP: { [id: string]: Tab[] } = {
title: 'Attributes',
},
],
'spl-token-metadata-interface': [
{
path: 'spl-token-metadata-interface',
slug: 'spl-token-metadata-interface',
title: 'SPL Token Metadata',
},
],
'spl-token:mint': [
{
path: 'transfers',
Expand Down Expand Up @@ -475,7 +482,8 @@ export type MoreTabs =
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree'
| 'program-interface';
| 'program-interface'
| 'spl-token-metadata-interface';

function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) {
return (
Expand Down Expand Up @@ -524,6 +532,16 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
tabs.push(...TABS_LOOKUP['address-lookup-table']);
}

// Add SPL Token Metadata Interface tab
console.log('Parsed data', parsedData);
if (
parsedData &&
parsedData.parsed.info &&
(parsedData.parsed.info.extensions as Record<string, string>[]).find(ext => ext.extension === 'metadataPointer')
) {
tabs.push(TABS_LOOKUP['spl-token-metadata-interface'][0]);
}

// Add the key for Metaplex NFTs
if (
parsedData &&
Expand Down
32 changes: 32 additions & 0 deletions app/address/[address]/spl-token-metadata-interface/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { LoadingCard } from '@components/common/LoadingCard';
import { Suspense } from 'react';
import React from 'react';

import { SplTokenMetadataInterfaceCard } from '@/app/components/account/SplTokenMetadataInterfaceCard';

type Props = Readonly<{
params: {
address: string;
};
}>;

function SplTokenMetadataInterfaceCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
if (!account) {
return onNotFound();
}
return (
<Suspense fallback={<LoadingCard message="Looking up MSA instructions in the anchor IDL" />}>
<SplTokenMetadataInterfaceCard mint={account.pubkey.toString()} />
</Suspense>
);
}

export default function SplTokenMetadataInterfacePageClient({ params: { address } }: Props) {
return <ParsedAccountRenderer address={address} renderComponent={SplTokenMetadataInterfaceCardRenderer} />;
}
21 changes: 21 additions & 0 deletions app/address/[address]/spl-token-metadata-interface/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import SplTokenMetadataInterfacePageClient from './page-client';

type Props = Readonly<{
params: {
address: string;
};
}>;

export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `SPL token metadata for ${props.params.address} on Solana`,
title: `SPL Token Metadata | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

export default function SplTokenMetadataInterfacePage(props: Props) {
return <SplTokenMetadataInterfacePageClient {...props} />;
}
209 changes: 209 additions & 0 deletions app/components/account/SplTokenMetadataInterfaceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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 useAsyncEffect from 'use-async-effect';

import { useMintAccountInfo } from '@/app/providers/accounts';

import { Address } from '../common/Address';

enum LoadingState {
Idle,
Loading,
MintMissing,
MetadataExtensionMissing,
MetadataAccountMissing,
MetadataFound,
}

function SplTokenMetadata({ metadata }: { metadata: TokenMetadata }) {
return (
<>
<tr>
<td>Update Authority</td>
<td>
{metadata.updateAuthority ? (
<Address pubkey={new PublicKey(metadata.updateAuthority.toString())} link />
) : (
'None'
)}
</td>
</tr>
<tr>
<td>Name</td>
<td>{metadata.name}</td>
</tr>
<tr>
<td>Symbol</td>
<td>{metadata.symbol}</td>
</tr>
<tr>
<td>Uri</td>
<td>{metadata.uri}</td>
</tr>
{metadata.additionalMetadata.map(([key, value], idx) => {
return (
<tr key={idx}>
<td>{key}</td>
<td>{value}</td>
</tr>
);
})}
</>
);
}

async function getTokenMetadata(connection: any, programId: PublicKey, metadataPointer: PublicKey) {
const ix = createEmitInstruction({ metadata: metadataPointer, programId });
const message = MessageV0.compile({
instructions: [ix],
// Use toly.sol's key as the payer key for the simulated transaction
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,
});
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 }) {
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);
}

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]);

// eslint-disable-next-line react-hooks/rules-of-hooks
useAsyncEffect(async () => {
if (extensions === null || metadataPointer === null) {
console.log('F');
setLoading(LoadingState.MetadataExtensionMissing);
return;
}

// 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);
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);
setLoading(LoadingState.MetadataFound);
} else {
setLoading(LoadingState.MetadataAccountMissing);
}
}, [mint, connection, mintInfo, metadataPointer]);

const metadataCard = useMemo(() => {
return (
<>
{loading === LoadingState.MetadataAccountMissing ? (
'Metadata account has no data'
) : loading === LoadingState.MetadataExtensionMissing ? (
'Metadata extension missing'
) : loading === LoadingState.MetadataFound ? (
metadata ? (
<SplTokenMetadata metadata={metadata} />
) : (
mint
)
) : (
'Loading'
)}
</>
);
}, [metadata, loading, mint]);

const card = useMemo(() => {
return (
<div className="card">
<div className="table-responsive mb-1">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted w-1">Field</th>
<th className="text-muted w-1">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Metadata Address</td>
<td>
{metadataPointer ? (
<Address pubkey={new PublicKey(metadataPointer)} link />
) : (
'Missing'
)}
</td>
</tr>
<tr>
<td>Metadata Authority</td>
<td>
{metadataAuthority ? (
<Address pubkey={new PublicKey(metadataAuthority)} link />
) : (
'None'
)}
</td>
</tr>
{metadataCard}
</tbody>
</table>
</div>
</div>
);
}, [loading, metadata, metadataAuthority, metadataPointer, mint]);

return card;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-account-compression": "^0.1.8",
"@solana/spl-token": "^0.1.8",
"@solana/spl-token-metadata": "^0.1.2",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-react-ui": "^0.9.34",
"@solana/wallet-adapter-wallets": "^0.19.24",
Expand Down
Loading

0 comments on commit 84e7948

Please sign in to comment.