Skip to content

Commit

Permalink
Add nifty asset program pages
Browse files Browse the repository at this point in the history
  • Loading branch information
febo committed Mar 6, 2024
1 parent 248801a commit 0631b6d
Show file tree
Hide file tree
Showing 13 changed files with 963 additions and 13 deletions.
83 changes: 74 additions & 9 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isNFTokenAccount, parseNFTokenCollectionAccount } from '@components/acc
import { NFTOKEN_ADDRESS } from '@components/account/nftoken/nftoken';
import { NFTokenAccountHeader } from '@components/account/nftoken/NFTokenAccountHeader';
import { NFTokenAccountSection } from '@components/account/nftoken/NFTokenAccountSection';
import { NiftyAssetAccountHeader } from '@/app/components/account/nifty-asset/AssetAccountHeader';
import { NonceAccountSection } from '@components/account/NonceAccountSection';
import { StakeAccountSection } from '@components/account/StakeAccountSection';
import { SysvarAccountSection } from '@components/account/SysvarAccountSection';
Expand Down Expand Up @@ -45,6 +46,9 @@ import useSWRImmutable from 'swr/immutable';
import { Base58EncodedAddress } from 'web3js-experimental';

import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';
import { isNiftyAssetAccount } from '@/app/components/account/nifty-asset/types';
import { ASSET_PROGRAM_ID, Asset, ExtensionType, getAssetAccountDataSerializer, getExtension } from '@nifty-oss/asset';
import { NiftyAssetAccountCard } from '@/app/components/account/nifty-asset/AssetAccountCard';

const IDENTICON_WIDTH = 64;

Expand Down Expand Up @@ -192,7 +196,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 +213,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 +245,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 All @@ -244,6 +270,10 @@ function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { ad
return <NFTokenAccountHeader account={account} />;
}

if (account && isNiftyAssetAccount(account.owner, account.data.raw)) {
return <NiftyAssetAccountHeader account={account} />;
}

if (isToken && !isTokenInfoLoading) {
let token;
let unverified = false;
Expand Down Expand Up @@ -314,7 +344,7 @@ function DetailsSections({
tab,
info,
tokenInfo,
isTokenInfoLoading
isTokenInfoLoading,
}: {
children: React.ReactNode;
pubkey: PublicKey;
Expand Down Expand Up @@ -348,7 +378,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 All @@ -371,6 +401,8 @@ function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: Ful
);
} else if (account.owner.toBase58() === NFTOKEN_ADDRESS) {
return <NFTokenAccountSection account={account} />;
} else if (account.owner.toBase58() === ASSET_PROGRAM_ID) {
return <NiftyAssetAccountCard account={account} />;
} else if (parsedData && isTokenProgramData(parsedData)) {
return <TokenAccountSection account={account} tokenAccount={parsedData.parsed} tokenInfo={tokenInfo} />;
} else if (parsedData && parsedData.program === 'nonce') {
Expand Down Expand Up @@ -425,7 +457,9 @@ export type MoreTabs =
| 'anchor-program'
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree';
| 'concurrent-merkle-tree'
| 'nifty-asset-metadata'
| 'nifty-asset-extensions';

function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) {
return (
Expand Down Expand Up @@ -467,12 +501,19 @@ 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`]);
}

Expand All @@ -488,8 +529,32 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
}
}

const isNiftyAsset = account && isNiftyAssetAccount(account.owner, account.data.raw);
if (isNiftyAsset && account.data.raw) {
const asset = account && (getAssetAccountDataSerializer().deserialize(account.data.raw)[0] as Asset);

if (asset.extensions.length > 0) {
const metadata = getExtension(asset, ExtensionType.Metadata);

if (metadata && metadata.uri.length > 0) {
tabs.push({
path: 'nifty-asset-metadata',
slug: 'nifty-asset-metadata',
title: 'Metadata',
});
}

tabs.push({
path: 'nifty-asset-extensions',
slug: 'nifty-asset-extensions',
title: 'Extensions',
});
}
}

if (
!isNFToken &&
!isNiftyAsset &&
(!parsedData || !(TOKEN_TABS_HIDDEN.includes(parsedData.program) || TOKEN_TABS_HIDDEN.includes(programTypeKey)))
) {
tabs.push({
Expand Down
26 changes: 26 additions & 0 deletions app/address/[address]/nifty-asset-extensions/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { NiftyAssetExtensionsCard } from '@/app/components/account/nifty-asset/AssetExtensionsCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { Asset, getAssetAccountDataSerializer } from '@nifty-oss/asset';
import React from 'react';

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

function NiftyAssetExtensionsCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const data = account?.data.raw;
const asset = data && (getAssetAccountDataSerializer().deserialize(data)[0] as Asset);

return asset && asset.extensions.length > 0 ? <NiftyAssetExtensionsCard asset={asset} /> : onNotFound();
}

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

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

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

export default function MetaplexNFTMetadataPage(props: Props) {
return <NiftyAssetExtensionsPageClient {...props} />;
}
34 changes: 34 additions & 0 deletions app/address/[address]/nifty-asset-metadata/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { NiftyAssetMetadataCard } from '@/app/components/account/nifty-asset/AssetMetadataCard';
import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
import { Asset, ExtensionType, getAssetAccountDataSerializer, getExtension } from '@nifty-oss/asset';
import React from 'react';

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

function NiftyAssetMetadataCardRenderer({
account,
onNotFound,
}: React.ComponentProps<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const data = account?.data.raw;
const asset = data && getAssetAccountDataSerializer().deserialize(data);

if (asset) {
const metadata = asset && getExtension(asset[0] as Asset, ExtensionType.Metadata);

if (metadata && metadata.uri) {
return <NiftyAssetMetadataCard asset={asset[0] as Asset} />;
}
}

return onNotFound();
}

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

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

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

export default function MetaplexNFTMetadataPage(props: Props) {
return <NiftyAssetMetadataPageClient {...props} />;
}
111 changes: 111 additions & 0 deletions app/components/account/nifty-asset/AssetAccountCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Address } from '@components/common/Address';
import { SolBalance } from '@components/common/SolBalance';
import { TableCardBody } from '@components/common/TableCardBody';
import { toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters';
import { Asset, State, getAssetAccountDataSerializer } from '@nifty-oss/asset';
import { Account } from '@providers/accounts';
import { useCluster } from '@providers/cluster';
import { addressLabel } from '@utils/tx';

export function NiftyAssetAccountCard({ account }: { account: Account }) {
const { cluster } = useCluster();

const label = addressLabel(account.pubkey.toBase58(), cluster);
const data = account.data.raw;
const asset = data && (getAssetAccountDataSerializer().deserialize(data)[0] as Asset);

return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Overview</h3>
</div>

<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-end">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end">
{account.lamports === 0 ? 'Account does not exist' : <SolBalance lamports={account.lamports} />}
</td>
</tr>

{account.space !== undefined && (
<tr>
<td>Allocated Data Size</td>
<td className="text-lg-end">{account.space} byte(s)</td>
</tr>
)}

{asset && (
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address pubkey={toWeb3JsPublicKey(asset.authority)} alignRight link />
</td>
</tr>
)}

{asset && (
<tr>
<td>Owner</td>
<td className="text-lg-end">
<Address pubkey={toWeb3JsPublicKey(asset.owner)} alignRight link />
</td>
</tr>
)}

{asset && (
<tr>
<td>Group</td>
<td className="text-lg-end">
{asset.group ? (
<Address pubkey={toWeb3JsPublicKey(asset.group)} alignRight link />
) : (
<div className="text-muted">None</div>
)}
</td>
</tr>
)}

{asset && (
<tr>
<td>Delegate</td>
<td className="text-lg-end">
{asset.delegate ? (
<Address pubkey={toWeb3JsPublicKey(asset.delegate.address)} alignRight link />
) : (
<div className="text-muted">None</div>
)}
</td>
</tr>
)}

{asset && (
<tr>
<td>State</td>
<td className="text-lg-end">
<h3 className="mb-0">
{asset.state === State.Unlocked ? (
<span className="badge badge-pill bg-info-soft">Unlocked</span>
) : (
<span className="badge badge-pill bg-danger-soft">Locked</span>
)}
</h3>
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
Loading

0 comments on commit 0631b6d

Please sign in to comment.