Skip to content

Commit

Permalink
add compression details card
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra committed May 24, 2024
1 parent 2f766f5 commit 1cd5863
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 70 deletions.
29 changes: 29 additions & 0 deletions app/address/[address]/compression/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

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

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

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} />;
}
8 changes: 7 additions & 1 deletion app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,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 @@ -538,6 +539,11 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
title: 'Attributes',
}
);
tabs.push({
path: 'compression',
slug: 'compression',
title: 'Compression',
});
}

const isNFToken = account && isNFTokenAccount(account);
Expand Down
131 changes: 131 additions & 0 deletions app/components/account/CompressedNFTInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Account, useAccountInfo, useFetchAccountInfo } from '@providers/accounts';
import { ConcurrentMerkleTreeAccount, MerkleTree } from '@solana/spl-account-compression';
import { PublicKey } from '@solana/web3.js';
import React from 'react';

import { useCluster } from '@/app/providers/cluster';
import {
CompressedNft,
CompressedNftProof,
useCompressedNft,
useCompressedNftProof,
} from '@/app/providers/compressed-nft';

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

export function CompressedNFTInfoCard({ account, onNotFound }: { account?: Account; onNotFound: () => never }) {
const { url } = useCluster();
const compressedNft = useCompressedNft({ address: account?.pubkey.toString() ?? '', url });
const proof = useCompressedNftProof({ address: account?.pubkey.toString() ?? '', url });

if (compressedNft && compressedNft.compression.compressed && proof) {
return <DasCompressionInfoCard proof={proof} compressedNft={compressedNft} />;
}
return onNotFound();
}

function DasCompressionInfoCard({ proof, compressedNft }: { proof: CompressedNftProof; compressedNft: CompressedNft }) {
const compressedInfo = compressedNft.compression;
const fetchAccountInfo = useFetchAccountInfo();
const treeAccountInfo = useAccountInfo(compressedInfo.tree);
const treeAddress = new PublicKey(compressedInfo.tree);

React.useEffect(() => {
fetchAccountInfo(treeAddress, 'raw');
}, [compressedInfo.tree]); // eslint-disable-line react-hooks/exhaustive-deps

const root = new PublicKey(proof.root);
const proofVerified = MerkleTree.verify(root.toBuffer(), {
leaf: new PublicKey(compressedNft.compression.asset_hash).toBuffer(),
leafIndex: compressedNft.compression.leaf_id,
proof: proof.proof.map(proofData => new PublicKey(proofData).toBuffer()),
root: root.toBuffer(),
});
const canopyDepth =
treeAccountInfo && treeAccountInfo.data && treeAccountInfo.data.data.raw
? ConcurrentMerkleTreeAccount.fromBuffer(treeAccountInfo.data.data.raw).getCanopyDepth()
: 0;
const proofSize = proof.proof.length - canopyDepth;
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Compression Info</h3>
</div>

<TableCardBody>
<tr>
<td>Concurrent Merkle Tree</td>
<td>
<Address pubkey={treeAddress} alignRight link raw />
</td>
</tr>
<tr>
<td>Current Tree Root {getVerifiedProofPill(proofVerified)}</td>
<td>
<Address pubkey={root} alignRight raw />
</td>
</tr>
<tr>
<td>Proof Size {getProofSizePill(proofSize)}</td>
<td className="text-lg-end">{proofSize}</td>
</tr>
<tr>
<td>Leaf Number</td>
<td className="text-lg-end">{compressedInfo.leaf_id}</td>
</tr>
<tr>
<td>Sequence Number of Last Update</td>
<td className="text-lg-end">{compressedInfo.seq}</td>
</tr>
<tr>
<td>Compressed Nft Hash</td>
<td>
<Address pubkey={new PublicKey(compressedInfo.asset_hash)} alignRight raw />
</td>
</tr>
<tr>
<td>Creators Hash</td>
<td>
<Address pubkey={new PublicKey(compressedInfo.creator_hash)} alignRight raw />
</td>
</tr>
<tr>
<td>Metadata Hash</td>
<td>
<Address pubkey={new PublicKey(compressedInfo.data_hash)} alignRight raw />
</td>
</tr>
</TableCardBody>
</div>
);
}

function getVerifiedProofPill(verified: boolean) {
return (
<div className={'d-inline-flex align-items-center ms-2'}>
<span className={`badge badge-pill bg-dark ${verified ? '' : 'bg-danger-soft'}`}>{`Proof ${
verified ? '' : 'Not'
} Verified`}</span>
</div>
);
}

function getProofSizePill(proofSize: number) {
let text: string;
let color = 'bg-dark';
if (proofSize == 0) {
text = 'No Proof Required';
} else if (proofSize > 8) {
text = `Composability Hazard`;
color = 'bg-danger-soft';
} else {
return <div />;
}

return (
<div className={'d-inline-flex align-items-center ms-2'}>
<span className={`badge badge-pill ${color}`}>{text}</span>
</div>
);
}
126 changes: 62 additions & 64 deletions app/components/account/ConcurrentMerkleTreeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,70 +26,68 @@ export function ConcurrentMerkleTreeCard({ data }: { data: Buffer }) {
</div>
</div>

<div className="card metadata-json-viewer m-4">
<TableCardBody>
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address pubkey={authority} alignRight raw />
</td>
</tr>
<tr>
<td>Creation Slot</td>
<td className="text-lg-end">
<Slot slot={creationSlot.toNumber()} link />
</td>
</tr>
<tr>
<td>Max Depth</td>
<td className="text-lg-end">
<span className="text-monospace">{treeHeight}</span>
</td>
</tr>
<tr>
<td>Max Buffer Size</td>
<td className="text-lg-end">
<span className="text-monospace">{maxBufferSize}</span>
</td>
</tr>
<tr>
<td>Canopy Depth</td>
<td className="text-lg-end">
<span className="text-monospace">{canopyDepth}</span>
</td>
</tr>
<tr>
<td>Current Sequence Number</td>
<td className="text-lg-end">
<span className="text-monospace">{seq.toString()}</span>
</td>
</tr>
<tr>
<td>Current Root</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(root)} alignRight raw />
</td>
</tr>
<tr>
<td>Current Number of Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{rightMostIndex}</span>
</td>
</tr>
<tr>
<td>Remaining Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{Math.pow(2, treeHeight) - rightMostIndex}</span>
</td>
</tr>
<tr>
<td>Max Possible Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{Math.pow(2, treeHeight)}</span>
</td>
</tr>
</TableCardBody>
</div>
<TableCardBody>
<tr>
<td>Authority</td>
<td className="text-lg-end">
<Address pubkey={authority} alignRight raw />
</td>
</tr>
<tr>
<td>Creation Slot</td>
<td className="text-lg-end">
<Slot slot={creationSlot.toNumber()} link />
</td>
</tr>
<tr>
<td>Max Depth</td>
<td className="text-lg-end">
<span className="text-monospace">{treeHeight}</span>
</td>
</tr>
<tr>
<td>Max Buffer Size</td>
<td className="text-lg-end">
<span className="text-monospace">{maxBufferSize}</span>
</td>
</tr>
<tr>
<td>Canopy Depth</td>
<td className="text-lg-end">
<span className="text-monospace">{canopyDepth}</span>
</td>
</tr>
<tr>
<td>Current Sequence Number</td>
<td className="text-lg-end">
<span className="text-monospace">{seq.toString()}</span>
</td>
</tr>
<tr>
<td>Current Root</td>
<td className="text-lg-end">
<Address pubkey={new PublicKey(root)} alignRight raw />
</td>
</tr>
<tr>
<td>Current Number of Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{rightMostIndex}</span>
</td>
</tr>
<tr>
<td>Remaining Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{Math.pow(2, treeHeight) - rightMostIndex}</span>
</td>
</tr>
<tr>
<td>Max Possible Leaves</td>
<td className="text-lg-end">
<span className="text-monospace">{Math.pow(2, treeHeight)}</span>
</td>
</tr>
</TableCardBody>
</div>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion app/components/account/MetaplexMetadataCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function CompressedMetadataCard({ compressedNft }: { compressedNft: CompressedNf
</div>

<div className="card metadata-json-viewer m-4">
<ReactJson src={metadataJson} theme={'solarized'} style={{ padding: 25 }} />
<ReactJson src={metadataJson} theme={'solarized'} style={{ padding: 25 }} name={false} />
</div>
</div>
</>
Expand Down
Loading

0 comments on commit 1cd5863

Please sign in to comment.