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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
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`);
}
51 changes: 51 additions & 0 deletions app/address/[address]/idl/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { IdlCard } from '@components/account/IdlCard';
import { useAnchorProgram } from '@providers/anchor';
import { useCluster } from '@providers/cluster';
import { useIdlFromProgramMetadataProgram } from '@providers/idl';
import { useState } from 'react';

export default function IdlPage({ params: { address } }: { params: { address: string } }) {
const { url } = useCluster();
const [activeTab, setActiveTab] = useState<'anchor' | 'metadata'>('anchor');
const { idl: anchorIdl } = useAnchorProgram(address, url);
const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(address, url);

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">
{activeTab === 'anchor' && anchorIdl && (
<IdlCard idl={anchorIdl} programId={address} title="Anchor IDL" />
)}
{activeTab === 'metadata' && metadataIdl && (
<IdlCard idl={metadataIdl} programId={address} title="Anchor IDL (Program metadata)" />
)}
</div>
</div>
);
}
66 changes: 53 additions & 13 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import { Address } from 'web3js-experimental';

import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';
import { useIdlFromProgramMetadataProgram } from '@/app/providers/idl';
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 +560,8 @@ export type MoreTabs =
| 'attributes'
| 'domains'
| 'security'
| 'anchor-program'
| 'program-metadata'
| 'idl'
| 'anchor-account'
| 'entries'
| 'concurrent-merkle-tree'
Expand Down Expand Up @@ -719,18 +722,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 +767,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 { idl } = useAnchorProgram(pubkey.toString(), url);
const anchorProgramPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const programMetadata = useProgramMetadata(pubkey.toString(), url);
const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const selectedLayoutSegment = useSelectedLayoutSegment();
const isActive = selectedLayoutSegment === tab.path;
if (!idl) {

// 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: anchorIdl } = useAnchorProgram(pubkey.toString(), url);
const { idl: metadataIdl } = useIdlFromProgramMetadataProgram(pubkey.toString(), url);
const path = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const selectedLayoutSegment = useSelectedLayoutSegment();
const isActive = selectedLayoutSegment === tab.path;

if (!anchorIdl && !metadataIdl) {
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,7 +1,6 @@
'use client';

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

Expand All @@ -10,12 +9,17 @@ 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);
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) {
console.log('No IDL found');
return null;
}
const spec = getIdlSpecType(idl);
Expand All @@ -25,7 +29,7 @@ export function AnchorProgramCard({ programId }: { programId: string }) {
<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