Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Metaplex SDK for Token Metadata #325

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,12 @@ function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { ad
let unverified = false;

// Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority
if (!parsedData?.nftData?.metadata.data.uri && tokenInfo) {
if (!parsedData?.nftData?.metadata.uri && tokenInfo) {
token = tokenInfo;
} else if (parsedData?.nftData) {
token = {
logoURI: parsedData?.nftData?.json?.image,
name: parsedData?.nftData?.json?.name ?? parsedData?.nftData.metadata.data.name,
name: parsedData?.nftData?.json?.name ?? parsedData?.nftData.metadata.name,
};
if (!tokenInfo?.verified) {
unverified = true;
Expand Down
4 changes: 3 additions & 1 deletion app/components/account/MetaplexMetadataCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NFTData } from '@providers/accounts';
import ReactJson from 'react-json-view';

export function MetaplexMetadataCard({ nftData }: { nftData: NFTData }) {
// Here we grossly stringify and parse the metadata to avoid the bigints which ReactJsonView does not support.
const json = JSON.parse(JSON.stringify(nftData.metadata, (_, v) => typeof v === 'bigint' ? v.toString() : v));
return (
<>
<div className="card">
Expand All @@ -14,7 +16,7 @@ export function MetaplexMetadataCard({ nftData }: { nftData: NFTData }) {
</div>

<div className="card metadata-json-viewer m-4">
<ReactJson src={nftData.metadata} theme={'solarized'} style={{ padding: 25 }} />
<ReactJson src={json} theme={'solarized'} style={{ padding: 25 }} />
</div>
</div>
</>
Expand Down
2 changes: 1 addition & 1 deletion app/components/account/MetaplexNFTAttributesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function MetaplexNFTAttributesCard({ nftData }: { nftData: NFTData }) {

async function fetchMetadataAttributes() {
try {
const response = await fetch(nftData.metadata.data.uri);
const response = await fetch(nftData.metadata.uri);
const metadata = await response.json();

// Verify if the attributes value is an array
Expand Down
36 changes: 20 additions & 16 deletions app/components/account/MetaplexNFTHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { InfoTooltip } from '@components/common/InfoTooltip';
import { ArtContent } from '@components/common/NFTArt';
import { programs } from '@metaplex/js';
import { Creator } from '@metaplex-foundation/mpl-token-metadata';
import { isSome } from '@metaplex-foundation/umi';
import * as Umi from '@metaplex-foundation/umi';
import { NFTData, useFetchAccountInfo, useMintAccountInfo } from '@providers/accounts';
import { EditionInfo } from '@providers/accounts/utils/getEditionInfo';
import { PublicKey } from '@solana/web3.js';
Expand All @@ -12,8 +14,13 @@ import useAsyncEffect from 'use-async-effect';

export function MetaplexNFTHeader({ nftData, address }: { nftData: NFTData; address: string }) {
const collection = nftData.metadata.collection;
const collectionAddress = collection?.key;
const collectionMintInfo = useMintAccountInfo(collectionAddress);
let collectionAddress: Umi.PublicKey | null = null;
let verified = false;
if (collection && isSome(collection)) {
collectionAddress = collection.value.key;
verified = collection.value.verified;
}
const collectionMintInfo = useMintAccountInfo(collectionAddress?.toString());
const fetchAccountInfo = useFetchAccountInfo();

React.useEffect(() => {
Expand All @@ -24,7 +31,7 @@ export function MetaplexNFTHeader({ nftData, address }: { nftData: NFTData; addr

const metadata = nftData.metadata;
const data = nftData.json;
const isVerifiedCollection = collection != null && collection?.verified && collectionMintInfo !== undefined;
const isVerifiedCollection = collection != null && verified && collectionMintInfo !== undefined;
const dropdownRef = createRef<HTMLButtonElement>();
useAsyncEffect(
async isMounted => {
Expand Down Expand Up @@ -53,13 +60,13 @@ export function MetaplexNFTHeader({ nftData, address }: { nftData: NFTData; addr
{<h6 className="header-pretitle ms-1">Metaplex NFT</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{metadata.data.name !== '' ? metadata.data.name : 'No NFT name was found'}
{metadata.name !== '' ? metadata.name : 'No NFT name was found'}
</h2>
{getEditionPill(nftData.editionInfo)}
{isVerifiedCollection ? getVerifiedCollectionPill() : null}
</div>
<h4 className="header-pretitle ms-1 mt-1 no-overflow-with-ellipsis">
{metadata.data.symbol !== '' ? metadata.data.symbol : 'No Symbol was found'}
{metadata.symbol !== '' ? metadata.symbol : 'No Symbol was found'}
</h4>
<div className="mb-2 mt-2">{getSaleTypePill(metadata.primarySaleHappened)}</div>
<div className="mb-3 mt-2">{getIsMutablePill(metadata.isMutable)}</div>
Expand All @@ -74,14 +81,13 @@ export function MetaplexNFTHeader({ nftData, address }: { nftData: NFTData; addr
>
Creators <ChevronDown size={15} className="align-text-top" />
</button>
<div className="dropdown-menu mt-2">{getCreatorDropdownItems(metadata.data.creators)}</div>
<div className="dropdown-menu mt-2">{getCreatorDropdownItems(isSome(metadata.creators) ? metadata.creators.value : [])}</div>
</div>
</div>
</div>
);
}

type Creator = programs.metadata.Creator;
function getCreatorDropdownItems(creators: Creator[] | null) {
const CreatorHeader = () => {
const creatorTooltip = 'Verified creators signed the metadata associated with this NFT when it was created.';
Expand Down Expand Up @@ -143,13 +149,12 @@ function getEditionPill(editionInfo: EditionInfo) {

return (
<div className={'d-inline-flex ms-2'}>
<span className="badge badge-pill bg-dark">{`${
edition && masterEdition
? `Edition ${edition.edition.toNumber()} / ${masterEdition.supply.toNumber()}`
: masterEdition
<span className="badge badge-pill bg-dark">{`${edition && masterEdition
? `Edition ${Number(edition.edition)} / ${Number(masterEdition.supply)}`
: masterEdition
? 'Master Edition'
: 'No Master Edition Information'
}`}</span>
}`}</span>
</div>
);
}
Expand All @@ -162,9 +167,8 @@ function getSaleTypePill(hasPrimarySaleHappened: boolean) {

return (
<div className={'d-inline-flex align-items-center'}>
<span className="badge badge-pill bg-dark">{`${
hasPrimarySaleHappened ? 'Secondary Market' : 'Primary Market'
}`}</span>
<span className="badge badge-pill bg-dark">{`${hasPrimarySaleHappened ? 'Secondary Market' : 'Primary Market'
}`}</span>
<InfoTooltip bottom text={hasPrimarySaleHappened ? secondaryMarketTooltip : primaryMarketTooltip} />
</div>
);
Expand Down
25 changes: 13 additions & 12 deletions app/components/account/TokenAccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Address } from '@components/common/Address';
import { Copyable } from '@components/common/Copyable';
import { LoadingCard } from '@components/common/LoadingCard';
import { TableCardBody } from '@components/common/TableCardBody';
import { isSome } from '@metaplex-foundation/umi';
import { Account, NFTData, TokenProgramData, useFetchAccountInfo } from '@providers/accounts';
import { TOKEN_2022_PROGRAM_ID } from '@providers/accounts/tokens';
import isMetaplexNFT from '@providers/accounts/utils/isMetaplexNFT';
Expand Down Expand Up @@ -198,8 +199,8 @@ function FungibleTokenMintAccountCard({
{tokenInfo
? 'Overview'
: account.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58()
? 'Token-2022 Mint'
: 'Token Mint'}
? 'Token-2022 Mint'
: 'Token Mint'}
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<RefreshCw className="align-text-top me-2" size={13} />
Expand Down Expand Up @@ -320,31 +321,31 @@ function NonFungibleTokenMintAccountCard({
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{nftData.editionInfo.masterEdition?.maxSupply && (
{nftData.editionInfo.masterEdition && isSome(nftData.editionInfo.masterEdition.maxSupply) && (
<tr>
<td>Max Total Supply</td>
<td className="text-lg-end">
{nftData.editionInfo.masterEdition.maxSupply.toNumber() === 0
{nftData.editionInfo.masterEdition.maxSupply.value === 0n
? 1
: nftData.editionInfo.masterEdition.maxSupply.toNumber()}
: Number(nftData.editionInfo.masterEdition.maxSupply.value)}
</td>
</tr>
)}
{nftData?.editionInfo.masterEdition?.supply && (
{nftData && nftData.editionInfo.masterEdition && nftData.editionInfo.masterEdition.supply && (
<tr>
<td>Current Supply</td>
<td className="text-lg-end">
{nftData.editionInfo.masterEdition.supply.toNumber() === 0
{nftData.editionInfo.masterEdition.supply === 0n
? 1
: nftData.editionInfo.masterEdition.supply.toNumber()}
: Number(nftData.editionInfo.masterEdition.supply)}
</td>
</tr>
)}
{!!collection?.verified && (
{collection && isSome(collection) && collection.value.verified && (
<tr>
<td>Verified Collection Address</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(collection.key)} alignRight link />
<Address pubkey={new PublicKey(collection.value.key)} alignRight link />
</td>
</tr>
)}
Expand Down Expand Up @@ -381,10 +382,10 @@ function NonFungibleTokenMintAccountCard({
</td>
</tr>
)}
{nftData?.metadata.data && (
{nftData?.metadata && (
<tr>
<td>Seller Fee</td>
<td className="text-lg-end">{`${nftData?.metadata.data.sellerFeeBasisPoints / 100}%`}</td>
<td className="text-lg-end">{`${nftData?.metadata.sellerFeeBasisPoints / 100}%`}</td>
</tr>
)}
</TableCardBody>
Expand Down
16 changes: 9 additions & 7 deletions app/components/common/Address.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';

import { Connection, programs } from '@metaplex/js';
import { fetchMetadata, findMetadataPda, Metadata } from '@metaplex-foundation/mpl-token-metadata';
import { publicKey } from '@metaplex-foundation/umi';
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { useCluster } from '@providers/cluster';
import { PublicKey } from '@solana/web3.js';
import { displayAddress, TokenLabelInfo } from '@utils/tx';
Expand Down Expand Up @@ -54,7 +56,7 @@ export function Address({

const metaplexData = useTokenMetadata(useMetadata, address);
if (metaplexData && metaplexData.data) {
addressLabel = metaplexData.data.data.name;
addressLabel = metaplexData.data.name;
}

const tokenInfo = useTokenInfo(fetchTokenLabelInfo, address);
Expand Down Expand Up @@ -94,18 +96,18 @@ export function Address({
);
}
const useTokenMetadata = (useMetadata: boolean | undefined, pubkey: string) => {
const [data, setData] = useState<programs.metadata.MetadataData>();
const [data, setData] = useState<Metadata>();
const { url } = useCluster();

useAsyncEffect(async isMounted => {
if (!useMetadata) return;
if (pubkey && !data) {
try {
const pda = await programs.metadata.Metadata.getPDA(pubkey);
const connection = new Connection(url);
const metadata = await programs.metadata.Metadata.load(connection, pda);
const umi = createUmi(url);
const pda = findMetadataPda(umi, { mint: publicKey(pubkey) });
const metadata = await fetchMetadata(umi, pda);
if (isMounted()) {
setData(metadata.data);
setData(metadata);
}
} catch {
if (isMounted()) {
Expand Down
11 changes: 5 additions & 6 deletions app/components/common/NFTArt.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Stream } from '@cloudflare/stream-react';
import { LoadingArtPlaceholder } from '@components/common/LoadingArtPlaceholder';
import ErrorLogo from '@img/logos-solana/dark-solana-logo.svg';
import { MetadataJson, MetaDataJsonCategory, MetadataJsonFile } from '@metaplex/js';
import { PublicKey } from '@solana/web3.js';
import { getLast } from '@utils/index';
import Image from 'next/image';
Expand Down Expand Up @@ -48,7 +47,7 @@ const VideoArtContent = ({
uri,
animationURL,
}: {
files?: (MetadataJsonFile | string)[];
files?: string[];
uri?: string;
animationURL?: string;
}) => {
Expand Down Expand Up @@ -107,7 +106,7 @@ const VideoArtContent = ({
return content;
};

const HTMLContent = ({ animationUrl, files }: { animationUrl?: string; files?: (MetadataJsonFile | string)[] }) => {
const HTMLContent = ({ animationUrl, files }: { animationUrl?: string; files?: string[] }) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const htmlURL = files && files.length > 0 && typeof files[0] === 'string' ? files[0] : animationUrl;
Expand Down Expand Up @@ -153,12 +152,12 @@ export const ArtContent = ({
files,
data,
}: {
category?: MetaDataJsonCategory;
category?: string;
pubkey?: PublicKey | string;
uri?: string;
animationURL?: string;
files?: (MetadataJsonFile | string)[];
data: MetadataJson | undefined;
files?: string[];
data: any | undefined;
}) => {
if (pubkey && data) {
uri = data.image;
Expand Down
30 changes: 16 additions & 14 deletions app/providers/accounts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';

import { MetadataJson, programs } from '@metaplex/js';
import { fetchMetadata, findMetadataPda, Metadata } from '@metaplex-foundation/mpl-token-metadata';
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { fromWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters';
import getEditionInfo, { EditionInfo } from '@providers/accounts/utils/getEditionInfo';
import * as Cache from '@providers/cache';
import { ActionType, FetchStatus } from '@providers/cache';
Expand Down Expand Up @@ -38,8 +40,6 @@ import { RewardsProvider } from './rewards';
import { TokensProvider } from './tokens';
export { useAccountHistory } from './history';

const Metadata = programs.metadata.Metadata;

export type StakeProgramData = {
program: 'stake';
parsed: StakeAccount;
Expand All @@ -53,16 +53,16 @@ export type UpgradeableLoaderAccountData = {
};

export type NFTData = {
metadata: programs.metadata.MetadataData;
json: MetadataJson | undefined;
metadata: Metadata;
json: any | undefined;
editionInfo: EditionInfo;
};

export function isTokenProgramData(data: { program: string }): data is TokenProgramData {
try {
assertIsTokenProgram(data.program);
return true;
} catch(e) {
} catch (e) {
return false;
}
}
Expand Down Expand Up @@ -138,7 +138,7 @@ class MultipleAccountFetcher {
private cluster: Cluster,
private url: string,
private dataMode: FetchAccountDataMode
) {}
) { }
fetch = (pubkey: PublicKey) => {
if (this.pubkeys !== undefined) this.pubkeys.add(pubkey.toBase58());
if (this.fetchTimeout === undefined) {
Expand Down Expand Up @@ -391,16 +391,18 @@ async function handleParsedAccountData(
try {
// Generate a PDA and check for a Metadata Account
if (parsed.type === 'mint') {
const metadata = await Metadata.load(connection, await Metadata.getPDA(accountKey));
const umi = createUmi(connection.rpcEndpoint);
const mint = fromWeb3JsPublicKey(accountKey);
const metadata = await fetchMetadata(umi, findMetadataPda(umi, { mint }));
if (metadata) {
// We have a valid Metadata account. Try and pull edition data.
const editionInfo = await getEditionInfo(metadata, connection);
const id = pubkeyToString(accountKey);
const metadataJSON = await getMetaDataJSON(id, metadata.data);
const metadataJSON = await getMetaDataJSON(id, metadata);
nftData = {
editionInfo,
json: metadataJSON,
metadata: metadata.data,
metadata: metadata,
};
}
}
Expand All @@ -421,10 +423,10 @@ const IMAGE_MIME_TYPE_REGEX = /data:image\/(svg\+xml|png|jpeg|gif)/g;

const getMetaDataJSON = async (
id: string,
metadata: programs.metadata.MetadataData
): Promise<MetadataJson | undefined> => {
metadata: Metadata
): Promise<any | undefined> => {
return new Promise(resolve => {
const uri = metadata.data.uri;
const uri = metadata.uri;
if (!uri) return resolve(undefined);

const processJson = (extended: any) => {
Expand All @@ -436,7 +438,7 @@ const getMetaDataJSON = async (
extended.image =
extended.image.startsWith('http') || IMAGE_MIME_TYPE_REGEX.test(extended.image)
? extended.image
: `${metadata.data.uri}/${extended.image}`;
: `${metadata.uri}/${extended.image}`;
}

return extended;
Expand Down
Loading