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

Adding Metaplex Core support #431

Closed
54 changes: 54 additions & 0 deletions app/address/[address]/core-metadata/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import { CoreMetadataCard } from '@components/account/CoreMetadataCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { AssetV1, CollectionV1, deserializeAssetV1, deserializeCollectionV1, Key } from '@metaplex-foundation/mpl-core';
import { lamports, RpcAccount } from '@metaplex-foundation/umi';
import { fromWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters';
import React, { useEffect } from 'react';

import { ErrorCard } from '@/app/components/common/ErrorCard';

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

function CoreMetadataCardRenderer({
account,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const [asset, setAsset] = React.useState<AssetV1 | CollectionV1 | null>(null);

useEffect(() => {
if (!account) {
return;
}

const rpcAccount: RpcAccount = {
data: Uint8Array.from(account.data.raw || new Uint8Array()),
executable: account.executable,
lamports: lamports(account.lamports),
owner: fromWeb3JsPublicKey(account.owner),
publicKey: fromWeb3JsPublicKey(account.pubkey),
};

if (rpcAccount.data[0] === Key.AssetV1) {
setAsset(deserializeAssetV1(rpcAccount));
} else if (rpcAccount.data[0] === Key.CollectionV1) {
setAsset(deserializeCollectionV1(rpcAccount));
}
}, [account]);

if (!account) {
return <ErrorCard text="Account is undefined" />;
} else if (!asset) {
return <ErrorCard text="Asset is undefined" />;
} else {
return <CoreMetadataCard asset={asset} />;
}
}

export default function MetaplexNFTMetadataPageClient({ params: { address } }: Props) {
return <ParsedAccountRenderer address={address} renderComponent={CoreMetadataCardRenderer} />;
}
21 changes: 21 additions & 0 deletions app/address/[address]/core-metadata/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 CoreNFTMetadataPageClient from './page-client';

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

export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Metadata for the Core NFT with address ${props.params.address} on Solana`,
title: `Core NFT Metadata | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

export default function CoreNFTMetadataPage(props: Props) {
return <CoreNFTMetadataPageClient {...props} />;
}
16 changes: 15 additions & 1 deletion app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import { ErrorBoundary } from 'react-error-boundary';
import { create } from 'superstruct';
import useSWRImmutable from 'swr/immutable';
import { Address } from 'web3js-experimental';

import { CoreAccountHeader } from '@/app/components/account/mplCore/CoreAccountHeader';
import { isCoreAccount } from '@/app/components/account/mplCore/isCoreAccount';
import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';
import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig';
Expand Down Expand Up @@ -274,6 +275,10 @@ function AccountHeader({
return <MetaplexNFTHeader nftData={parsedData.nftData} address={address} />;
}

if (account && isCoreAccount(account)) {
return <CoreAccountHeader account={account} />;
}

const nftokenNFT = account && isNFTokenAccount(account);
if (nftokenNFT && account) {
return <NFTokenAccountHeader account={account} />;
Expand Down Expand Up @@ -666,6 +671,15 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
});
}

const isCoreAsset = account && isCoreAccount(account);
if (isCoreAsset) {
tabs.push({
path: 'core-metadata',
slug: 'metadata',
title: 'Metadata',
});
}

if (account.owner.toBase58() === ACCOUNT_COMPRESSION_ID.toBase58()) {
tabs.push(TABS_LOOKUP['spl-account-compression'][0]);
}
Expand Down
24 changes: 24 additions & 0 deletions app/components/account/CoreMetadataCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AssetV1, CollectionV1 } from '@metaplex-foundation/mpl-core';
import ReactJson from 'react-json-view';

export function CoreMetadataCard({ asset }: { asset: AssetV1 | CollectionV1 | null }) {
// Here we grossly stringify and parse the metadata to avoid the bigints which ReactJsonView does not support.
const json = JSON.parse(JSON.stringify(asset, (_, v) => typeof v === 'bigint' ? v.toString() : v));
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Metaplex Core Metadata</h3>
</div>
</div>
</div>

<div className="card metadata-json-viewer m-4">
<ReactJson name={false} src={json} theme={'solarized'} style={{ padding: 25 }} />
</div>
</div>
</>
);
}
13 changes: 10 additions & 3 deletions app/components/account/MetaplexMetadataCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CompressedNft, useCompressedNft, useMetadataJsonLink } from '@/app/prov
export function MetaplexMetadataCard({ account, onNotFound }: { account?: Account; onNotFound: () => never }) {
const { url } = useCluster();
const compressedNft = useCompressedNft({ address: account?.pubkey.toString() ?? '', url });
console.log(compressedNft);

const parsedData = account?.data?.parsed;
if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
Expand All @@ -16,10 +17,14 @@ export function MetaplexMetadataCard({ account, onNotFound }: { account?: Accoun
}
return onNotFound();
}
return <NormalMetadataCard nftData={parsedData.nftData} />;

// Here we grossly stringify and parse the metadata to avoid the bigints which ReactJsonView does not support.
const json = JSON.parse(JSON.stringify(parsedData.nftData.metadata, (_, v) => typeof v === 'bigint' ? v.toString() : v));

return <NormalMetadataCard json={json} />;
}

function NormalMetadataCard({ nftData }: { nftData: NFTData }) {
function NormalMetadataCard({ json }: { json: any }) {
return (
<>
<div className="card">
Expand All @@ -32,15 +37,17 @@ function NormalMetadataCard({ 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>
</>
);
}

function CompressedMetadataCard({ compressedNft }: { compressedNft: CompressedNft }) {
console.log("is compressed", compressedNft.compression.compressed);
const metadataJson = useMetadataJsonLink(compressedNft.content.json_uri);
console.log("metadata json", metadataJson);
return (
<>
<div className="card">
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 @@ -23,7 +23,7 @@ export function MetaplexNFTAttributesCard({ account, onNotFound }: { account?: A
}
return onNotFound();
}
return <NormalMetaplexNFTAttributesCard metadataUri={parsedData.nftData.metadata.data.uri} />;
return <NormalMetaplexNFTAttributesCard metadataUri={parsedData.nftData.metadata.uri} />;
}

function NormalMetaplexNFTAttributesCard({ metadataUri }: { metadataUri: string }) {
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;
export 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 @@ -204,8 +205,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 @@ -326,31 +327,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 @@ -387,10 +388,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
33 changes: 33 additions & 0 deletions app/components/account/mplCore/CoreAccountHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Key } from '@metaplex-foundation/mpl-core';
import * as Umi from '@metaplex-foundation/umi';
import { fromWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters';
import { Account } from '@providers/accounts';
import React, { useEffect } from 'react';

import { ErrorCard } from '../../common/ErrorCard';
import { CoreAssetHeader } from './CoreAssetHeader';
import { CoreCollectionHeader } from './CoreCollectionHeader';

export function CoreAccountHeader({ account }: { account: Account }) {
const [umiAccount, setUmiAccount] = React.useState<Umi.RpcAccount | null>(null);

useEffect(() => {
const rpcAccount: Umi.RpcAccount = {
data: Uint8Array.from(account.data.raw || new Uint8Array()),
executable: account.executable,
lamports: Umi.lamports(account.lamports),
owner: fromWeb3JsPublicKey(account.owner),
publicKey: fromWeb3JsPublicKey(account.pubkey),
};

setUmiAccount(rpcAccount);
}, [account]);

if (umiAccount && umiAccount.data[0] === Key.AssetV1) {
return <CoreAssetHeader account={account} />
} else if (umiAccount && umiAccount.data[0] === Key.CollectionV1) {
return <CoreCollectionHeader account={account} />
} else {
return <ErrorCard text="Invalid Core Account" />
}
}
Loading
Loading