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

Metadata program idl #425

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

## Development

Contributing to the Explorer requires `pnpm` version `9.10.0`.
Contributing to the Explorer requires `pnpm` version `9.10.0`.
Once you have this version of `pnpm`, you can continue with the following steps.


- Copy `.env.example` into `.env` & fill out the fields with custom RPC urls \
from a Solana RPC provider. You should not use `https://api.mainnet-beta.solana.com` \
or `https://api.devnet.solana.com` or else you will get rate-limited. These are public \
Expand All @@ -28,17 +27,19 @@ Once you have this version of `pnpm`, you can continue with the following steps.
The page will reload if you make edits. \
You will also see any lint errors in the console.

- `npm run lint -- --fix` \
Lints the code and fixes any linting errors.

- (Optional) `pnpm test` \
Launches the test runner in the interactive watch mode.<br />

## Troubleshooting

Still can't run the explorer with `pnpm dev`?
Still can't run the explorer with `pnpm dev`?
Seeing sass dependency errors?
Make sure you have `pnpm` version `9.10.0`, `git stash` your changes, then blow reset to master with `rm -rf node_modules && git reset --hard HEAD`.
Now running `pnpm i` followed by `pnpm dev` should work. If it is working, don't forget to reapply your changes with `git stash pop`.


# Disclaimer

All claims, content, designs, algorithms, estimates, roadmaps,
Expand Down
20 changes: 3 additions & 17 deletions app/address/[address]/anchor-program/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import { AnchorProgramCard } from '@components/account/AnchorProgramCard';
import { LoadingCard } from '@components/common/LoadingCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';
import { Suspense } from 'react';
import { redirect } from 'next/navigation';

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

export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `The Interface Definition Language (IDL) file for the Anchor program at address ${props.params.address} on Solana`,
title: `Anchor Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

// Redirect to the new IDL page
export default function AnchorProgramIDLPage({ params: { address } }: Props) {
return (
<Suspense fallback={<LoadingCard message="Loading anchor program IDL" />}>
<AnchorProgramCard programId={address} />
</Suspense>
);
redirect(`/address/${address}/idl`);
}
71 changes: 71 additions & 0 deletions app/address/[address]/idl/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client';

import { IdlCard } from '@components/account/IdlCard';
import { useIdlFromAnchorProgramSeed } from '@providers/anchor';
import { useCluster } from '@providers/cluster';
import { useIdlFromMetadataProgram } from '@providers/idl';
import { Suspense, useEffect, useState } from 'react';

export default function IdlPage({ params: { address } }: { params: { address: string } }) {
const { url } = useCluster();
const anchorIdl = useIdlFromAnchorProgramSeed(address, url, false);
const metadataIdl = useIdlFromMetadataProgram(address, url, false);

const [activeTab, setActiveTab] = useState<'anchor' | 'metadata'>('anchor');

useEffect(() => {
// Show whatever tab is available
if (!anchorIdl && metadataIdl) {
setActiveTab('metadata');
}
}, [anchorIdl, metadataIdl]);

return (
<div className="card">
<div className="card-header">
<ul className="nav nav-tabs card-header-tabs">
{anchorIdl && (
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'anchor' ? 'active' : ''}`}
onClick={() => setActiveTab('anchor')}
>
Anchor IDL
</button>
</li>
)}
{metadataIdl && (
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'metadata' ? 'active' : ''}`}
onClick={() => setActiveTab('metadata')}
>
Program Metadata IDL
</button>
</li>
)}
</ul>
</div>
<div className="card-body">
<Suspense fallback={<div>Loading...</div>}>
{activeTab === 'anchor' && anchorIdl && <AnchorIdlCard programId={address} url={url} />}
{activeTab === 'metadata' && metadataIdl && (
<ProgramMetadataIdlCard url={url} programId={address} />
)}
</Suspense>
</div>
</div>
);
}

function ProgramMetadataIdlCard({ programId, url }: { programId: string; url: string }) {
const idl = useIdlFromMetadataProgram(programId, url, true);

return <IdlCard idl={idl ?? ({} as any)} programId={programId} title="Program Metadata IDL" />;
}

function AnchorIdlCard({ programId, url }: { programId: string; url: string }) {
const idl = useIdlFromAnchorProgramSeed(programId, url, true);

return <IdlCard idl={idl ?? ({} as any)} programId={programId} title="Anchor IDL" />;
}
61 changes: 50 additions & 11 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { Address } from 'web3js-experimental';

import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';
import { useProgramMetadata } from '@/app/providers/program-metadata';
import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig';
import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';
import { MintAccountInfo } from '@/app/validators/accounts/token';
Expand Down Expand Up @@ -558,7 +559,8 @@ export type MoreTabs =
| 'attributes'
| 'domains'
| 'security'
| 'anchor-program'
| 'program-metadata'
| 'idl'
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree'
Expand Down Expand Up @@ -719,18 +721,32 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) {
tab: programMultisigTab,
});

const anchorProgramTab: Tab = {
path: 'anchor-program',
slug: 'anchor-program',
title: 'Anchor Program IDL',
const idlTab: Tab = {
path: 'idl',
slug: 'idl',
title: 'IDL',
};
tabComponents.push({
component: (
<React.Suspense key={anchorProgramTab.slug} fallback={<></>}>
<AnchorProgramIdlLink tab={anchorProgramTab} address={pubkey.toString()} pubkey={pubkey} />
<React.Suspense key={idlTab.slug} fallback={<></>}>
<IdlDataLink tab={idlTab} address={pubkey.toString()} pubkey={pubkey} />
</React.Suspense>
),
tab: anchorProgramTab,
tab: idlTab,
});

const programMetadataTab: Tab = {
path: 'program-metadata',
slug: 'program-metadata',
title: 'Program Metadata',
};
tabComponents.push({
component: (
<React.Suspense key={programMetadataTab.slug} fallback={<></>}>
<ProgramMetaDataLink tab={programMetadataTab} address={pubkey.toString()} pubkey={pubkey} />
</React.Suspense>
),
tab: programMetadataTab,
});

const accountDataTab: Tab = {
Expand All @@ -750,19 +766,42 @@ function getCustomLinkedTabs(pubkey: PublicKey, account: Account) {
return tabComponents;
}

function AnchorProgramIdlLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) {
function ProgramMetaDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) {
const { url } = useCluster();
const programMetadata = useProgramMetadata(pubkey.toString(), url);
const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const selectedLayoutSegment = useSelectedLayoutSegment();
const isActive = selectedLayoutSegment === tab.path;

// Don't show the tab if there's no metadata
if (!programMetadata) {
return null;
}

return (
<li key={tab.slug} className="nav-item">
<Link className={`${isActive ? 'active ' : ''}nav-link`} href={path}>
{tab.title}
</Link>
</li>
);
}

function IdlDataLink({ tab, address, pubkey }: { tab: Tab; address: string; pubkey: PublicKey }) {
const { url } = useCluster();
const { idl } = useAnchorProgram(pubkey.toString(), url);
const anchorProgramPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const selectedLayoutSegment = useSelectedLayoutSegment();
const isActive = selectedLayoutSegment === tab.path;

// Will be null if no anchor and no program metadata IDL
if (!idl) {
return null;
}

return (
<li key={tab.slug} className="nav-item">
<Link className={`${isActive ? 'active ' : ''}nav-link`} href={anchorProgramPath}>
<Link className={`${isActive ? 'active ' : ''}nav-link`} href={path}>
{tab.title}
</Link>
</li>
Expand Down
27 changes: 27 additions & 0 deletions app/address/[address]/program-metadata/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { LoadingCard } from '@components/common/LoadingCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';
import { Suspense } from 'react';

import { ProgramMetadataCard } from '@/app/components/account/ProgramMetadataCard';

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

export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `This is the meta data uploaded by the program authority for program ${props.params.address} on Solana`,
title: `Program Metadata | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

export default function ProgramMetadataPage({ params: { address } }: Props) {
return (
<Suspense fallback={<LoadingCard message="Loading program metadata" />}>
<ProgramMetadataCard programId={address} />
</Suspense>
);
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
'use client';

import { useAnchorProgram } from '@providers/anchor';
import { useCluster } from '@providers/cluster';
import { Idl } from '@coral-xyz/anchor';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import ReactJson from 'react-json-view';

import { getIdlSpecType } from '@/app/utils/convertLegacyIdl';

import { DownloadableButton } from '../common/Downloadable';
import { IDLBadge } from '../common/IDLBadge';

export function AnchorProgramCard({ programId }: { programId: string }) {
const { url } = useCluster();
const { idl } = useAnchorProgram(programId, url);
// Necessary to avoid hydration errors
const ReactJson = dynamic(() => import('react-json-view'), {
loading: () => <div>Loading IDL...</div>,
ssr: false,
});

interface Props {
idl: Idl;
programId: string;
title?: string;
}

export function IdlCard({ idl, programId, title = 'Program IDL' }: Props) {
const [collapsedValue, setCollapsedValue] = useState<boolean | number>(1);

if (!idl) {
return null;
}

const spec = getIdlSpecType(idl);

return (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Anchor IDL</h3>
<h3 className="card-header-title">{title}</h3>
</div>
<div className="col-auto btn btn-sm btn-primary d-flex align-items-center">
<DownloadableButton
Expand Down
Loading
Loading