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

Add cNFT support to Explorer #338

Merged
merged 7 commits into from
May 29, 2024
Merged
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
15 changes: 8 additions & 7 deletions app/address/[address]/attributes/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import { MetaplexNFTAttributesCard } from '@components/account/MetaplexNFTAttributesCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { isTokenProgramData } from '@providers/accounts';
import React from 'react';
import React, { Suspense } from 'react';

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

type Props = Readonly<{
params: {
Expand All @@ -15,11 +16,11 @@ function MetaplexNFTAttributesCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
return onNotFound();
}
return <MetaplexNFTAttributesCard nftData={parsedData.nftData} />;
return (
<Suspense fallback={<LoadingCard />}>
{<MetaplexNFTAttributesCard account={account} onNotFound={onNotFound} />}
</Suspense>
);
}

export default function MetaplexNFTAttributesPageClient({ params: { address } }: Props) {
Expand Down
28 changes: 28 additions & 0 deletions app/address/[address]/compression/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

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

import { CompressedNFTInfoCard } from '@/app/components/account/CompressedNFTInfoCard';
import { LoadingCard } from '@/app/components/common/LoadingCard';

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

function CompressionCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
return (
<Suspense fallback={<LoadingCard />}>
{<CompressedNFTInfoCard account={account} onNotFound={onNotFound} />}
</Suspense>
);
}

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

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

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

export default function CompressionPage(props: Props) {
return <CompressionPageClient {...props} />;
}
93 changes: 81 additions & 12 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ import { FEATURE_PROGRAM_ID } from '@utils/parseFeatureAccount';
import { useClusterPath } from '@utils/url';
import Link from 'next/link';
import { redirect, useSelectedLayoutSegment } from 'next/navigation';
import React, { PropsWithChildren } from 'react';
import React, { PropsWithChildren, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import useSWRImmutable from 'swr/immutable';
import { Base58EncodedAddress } from 'web3js-experimental';

import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';

const IDENTICON_WIDTH = 64;
Expand Down Expand Up @@ -192,7 +194,9 @@ function AddressLayoutInner({ children, params: { address } }: Props) {
const infoParsed = info?.data?.data.parsed;

const { data: fullTokenInfo, isLoading: isFullTokenInfoLoading } = useSWRImmutable(
infoStatus === FetchStatus.Fetched && infoParsed && isTokenProgramData(infoParsed) && pubkey ? ['get-full-token-info', address, cluster, url] : null,
infoStatus === FetchStatus.Fetched && infoParsed && isTokenProgramData(infoParsed) && pubkey
? ['get-full-token-info', address, cluster, url]
: null,
fetchFullTokenInfo
);

Expand All @@ -207,13 +211,23 @@ function AddressLayoutInner({ children, params: { address } }: Props) {
<div className="container mt-n3">
<div className="header">
<div className="header-body">
<AccountHeader address={address} account={info?.data} tokenInfo={fullTokenInfo} isTokenInfoLoading={isFullTokenInfoLoading} />
<AccountHeader
address={address}
account={info?.data}
tokenInfo={fullTokenInfo}
isTokenInfoLoading={isFullTokenInfoLoading}
/>
</div>
</div>
{!pubkey ? (
<ErrorCard text={`Address "${address}" is not valid`} />
) : (
<DetailsSections info={info} pubkey={pubkey} tokenInfo={fullTokenInfo} isTokenInfoLoading={isFullTokenInfoLoading}>
<DetailsSections
info={info}
pubkey={pubkey}
tokenInfo={fullTokenInfo}
isTokenInfoLoading={isFullTokenInfoLoading}
>
{children}
</DetailsSections>
)}
Expand All @@ -229,7 +243,17 @@ export default function AddressLayout({ children, params }: Props) {
);
}

function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { address: string; account?: Account, tokenInfo?: FullTokenInfo, isTokenInfoLoading: boolean }) {
function AccountHeader({
address,
account,
tokenInfo,
isTokenInfoLoading,
}: {
address: string;
account?: Account;
tokenInfo?: FullTokenInfo;
isTokenInfoLoading: boolean;
}) {
const mintInfo = useMintAccountInfo(address);

const parsedData = account?.data.parsed;
Expand Down Expand Up @@ -300,12 +324,22 @@ function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { ad
);
}

return (
const fallback = (
<>
<h6 className="header-pretitle">Details</h6>
<h2 className="header-title">Account</h2>
</>
);
if (account) {
return (
<ErrorBoundary fallback={fallback}>
<Suspense fallback={fallback}>
<CompressedNftAccountHeader account={account} />
</Suspense>
</ErrorBoundary>
);
}
return fallback;
}

function DetailsSections({
Expand All @@ -314,7 +348,7 @@ function DetailsSections({
tab,
info,
tokenInfo,
isTokenInfoLoading
isTokenInfoLoading,
}: {
children: React.ReactNode;
pubkey: PublicKey;
Expand Down Expand Up @@ -348,7 +382,7 @@ function DetailsSections({
);
}

function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: FullTokenInfo }) {
function InfoSection({ account, tokenInfo }: { account: Account; tokenInfo?: FullTokenInfo }) {
const parsedData = account.data.parsed;
const rawData = account.data.raw;

Expand Down Expand Up @@ -392,7 +426,14 @@ function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: Ful
} else if (account.owner.toBase58() === FEATURE_PROGRAM_ID) {
return <FeatureAccountSection account={account} />;
} else {
return <UnknownAccountCard account={account} />;
const fallback = <UnknownAccountCard account={account} />;
return (
<ErrorBoundary fallback={fallback}>
<Suspense fallback={fallback}>
<CompressedNftCard account={account} />
</Suspense>
</ErrorBoundary>
);
}
}

Expand Down Expand Up @@ -425,7 +466,8 @@ export type MoreTabs =
| 'anchor-program'
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree';
| 'concurrent-merkle-tree'
| 'compression';

function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) {
return (
Expand Down Expand Up @@ -467,15 +509,42 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
}

// Add the key for address lookup tables
if (account.data.raw && isAddressLookupTableAccount(account.owner.toBase58() as Base58EncodedAddress, account.data.raw)) {
if (
account.data.raw &&
isAddressLookupTableAccount(account.owner.toBase58() as Base58EncodedAddress, account.data.raw)
) {
tabs.push(...TABS_LOOKUP['address-lookup-table']);
}

// Add the key for Metaplex NFTs
if (parsedData && (programTypeKey === 'spl-token:mint' || programTypeKey == 'spl-token-2022:mint') && (parsedData as TokenProgramData).nftData) {
if (
parsedData &&
(programTypeKey === 'spl-token:mint' || programTypeKey == 'spl-token-2022:mint') &&
(parsedData as TokenProgramData).nftData
) {
tabs.push(...TABS_LOOKUP[`${programTypeKey}:metaplexNFT`]);
}

if (!tabs.find(tab => tab.slug === 'metadata')) {
tabs.push(
{
path: 'metadata',
slug: 'metadata',
title: 'Metadata',
},
{
path: 'attributes',
slug: 'attributes',
title: 'Attributes',
}
);
tabs.push({
path: 'compression',
slug: 'compression',
title: 'Compression',
});
}

const isNFToken = account && isNFTokenAccount(account);
if (isNFToken) {
const collection = parseNFTokenCollectionAccount(account);
Expand Down
15 changes: 8 additions & 7 deletions app/address/[address]/metadata/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import { MetaplexMetadataCard } from '@components/account/MetaplexMetadataCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { isTokenProgramData } from '@providers/accounts';
import React from 'react';
import React, { Suspense } from 'react';

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

type Props = Readonly<{
params: {
Expand All @@ -15,11 +16,11 @@ function MetaplexMetadataCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) {
return onNotFound();
}
return <MetaplexMetadataCard nftData={parsedData.nftData} />;
return (
<Suspense fallback={<LoadingCard />}>
{<MetaplexMetadataCard account={account} onNotFound={onNotFound} />}
</Suspense>
);
}

export default function MetaplexNFTMetadataPageClient({ params: { address } }: Props) {
Expand Down
Loading
Loading