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 support for parsing IDLs stored in .solana.idl section of ELF #348

Merged
merged 4 commits into from
Jun 17, 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
23 changes: 13 additions & 10 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,12 @@ function AccountHeader({
let token: { logoURI?: string; name?: string } = {};
let unverified = false;

const metadataExtension = mintInfo?.extensions?.find(({ extension }: { extension: string }) => extension === 'tokenMetadata');
const metadataPointerExtension = mintInfo?.extensions?.find(({ extension }: { extension: string }) => extension === 'metadataPointer');

const metadataExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'tokenMetadata'
);
const metadataPointerExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'metadataPointer'
);

if (metadataPointerExtension && metadataExtension) {
const tokenMetadata = create(metadataExtension.state, TokenMetadata);
Expand All @@ -286,9 +289,9 @@ function AccountHeader({
// Handles the basic case where MetadataPointer is reference the Token Metadata extension directly
// Does not handle the case where MetadataPointer is pointing at a separate account.
if (metadataAddress?.toString() === address) {
token.name = tokenMetadata.name
token.name = tokenMetadata.name;
}
}
}
// Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority
else if (!parsedData?.nftData?.metadata.data.uri && tokenInfo) {
token = tokenInfo;
Expand Down Expand Up @@ -630,7 +633,7 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
tabComponents.push({
component: (
<React.Suspense key={anchorProgramTab.slug} fallback={<></>}>
<AnchorProgramLink tab={anchorProgramTab} address={pubkey.toString()} pubkey={pubkey} />
<AnchorProgramIdlLink tab={anchorProgramTab} address={pubkey.toString()} pubkey={pubkey} />
</React.Suspense>
),
tab: anchorProgramTab,
Expand All @@ -653,13 +656,13 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
return tabComponents;
}

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

Expand All @@ -674,7 +677,7 @@ function AnchorProgramLink({ tab, address, pubkey }: { tab: Tab; address: string

function AccountDataLink({ address, tab, programId }: { address: string; tab: Tab; programId: PublicKey }) {
const { url } = useCluster();
const accountAnchorProgram = useAnchorProgram(programId.toString(), url);
const { program: accountAnchorProgram } = useAnchorProgram(programId.toString(), url);
const accountDataPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
const selectedLayoutSegment = useSelectedLayoutSegment();
const isActive = selectedLayoutSegment === tab.path;
Expand Down
2 changes: 1 addition & 1 deletion app/components/account/AnchorAccountCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React, { useMemo } from 'react';
export function AnchorAccountCard({ account }: { account: Account }) {
const { lamports } = account;
const { url } = useCluster();
const anchorProgram = useAnchorProgram(account.owner.toString(), url);
const { program: anchorProgram } = useAnchorProgram(account.owner.toString(), url);
const rawData = account.data.raw;
const programName = getAnchorProgramName(anchorProgram) || 'Unknown Program';

Expand Down
6 changes: 3 additions & 3 deletions app/components/account/AnchorProgramCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import ReactJson from 'react-json-view';

export function AnchorProgramCard({ programId }: { programId: string }) {
const { url } = useCluster();
const program = useAnchorProgram(programId, url);
const { idl } = useAnchorProgram(programId, url);

if (!program) {
if (!idl) {
return null;
}

Expand All @@ -24,7 +24,7 @@ export function AnchorProgramCard({ programId }: { programId: string }) {
</div>

<div className="card metadata-json-viewer m-4">
<ReactJson src={program.idl} theme={'solarized'} style={{ padding: 25 }} collapsed={1} />
<ReactJson src={idl} theme={'solarized'} style={{ padding: 25 }} collapsed={1} name={null} />
</div>
</div>
</>
Expand Down
2 changes: 1 addition & 1 deletion app/components/transaction/InstructionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ function InstructionCard({
url: string;
}) {
const key = `${index}-${childIndex}`;
const anchorProgram = useAnchorProgram(ix.programId.toString(), url);
const { program: anchorProgram } = useAnchorProgram(ix.programId.toString(), url);

if ('parsed' in ix) {
const props = {
Expand Down
107 changes: 98 additions & 9 deletions app/providers/anchor.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,99 @@
import { NodeWallet } from '@metaplex/js';
import { Idl, Program, Provider } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as elfy from 'elfy';
import pako from 'pako';
import { useEffect, useMemo } from 'react';

import { useAccountInfo, useFetchAccountInfo } from './accounts';

const cachedAnchorProgramPromises: Record<
string,
void | { __type: 'promise'; promise: Promise<void> } | { __type: 'result'; result: Program<Idl> | null }
void | { __type: 'promise'; promise: Promise<void> } | { __type: 'result'; result: Idl | null }
> = {};

export function useAnchorProgram(programAddress: string, url: string): Program | null {
function useIdlFromSolanaProgramBinary(programAddress: string): Idl | null {
const fetchAccountInfo = useFetchAccountInfo();
const programInfo = useAccountInfo(programAddress);
const programDataAddress: string | undefined = programInfo?.data?.data.parsed?.parsed.info['programData'];
const programDataInfo = useAccountInfo(programDataAddress);

useEffect(() => {
if (!programInfo) {
fetchAccountInfo(new PublicKey(programAddress), 'parsed');
}
}, [programAddress, fetchAccountInfo, programInfo]);

useEffect(() => {
if (programDataAddress && !programDataInfo) {
fetchAccountInfo(new PublicKey(programDataAddress), 'raw');
}
}, [programDataAddress, fetchAccountInfo, programDataInfo]);

const param = useMemo(() => {
if (programDataInfo && programDataInfo.data && programDataInfo.data.data.raw) {
const offset =
(programInfo?.data?.owner.toString() ?? '') === 'BPFLoaderUpgradeab1e11111111111111111111111' ? 45 : 0;
const raw = Buffer.from(programDataInfo.data.data.raw.slice(offset));

try {
return parseIdlFromElf(raw);
} catch (e) {
return null;
}
}
return null;
}, [programDataInfo, programInfo]);
return param;
}

function parseIdlFromElf(elfBuffer: any) {
const elf = elfy.parse(elfBuffer);
const solanaIdlSection = elf.body.sections.find((section: any) => section.name === '.solana.idl');
if (!solanaIdlSection) {
throw new Error('.solana.idl section not found');
}

// Extract the section data
const solanaIdlData = solanaIdlSection.data;

// Parse the section data
solanaIdlData.readUInt32LE(4);
const ptr = solanaIdlData.readUInt32LE(4);
const size = solanaIdlData.readBigUInt64LE(8);

// Get the compressed bytes
const byteRange = elfBuffer.slice(ptr, ptr + Number(size));

// Decompress the IDL
try {
const inflatedIdl = JSON.parse(new TextDecoder().decode(pako.inflate(byteRange)));
return inflatedIdl;
} catch (err) {
console.error('Failed to decompress data:', err);
return null;
}
}

function getProvider(url: string) {
return new Provider(new Connection(url), new NodeWallet(Keypair.generate()), {});
}

function useIdlFromAnchorProgramSeed(programAddress: string, url: string): Idl | null {
const key = `${programAddress}-${url}`;
const cacheEntry = cachedAnchorProgramPromises[key];

if (cacheEntry === undefined) {
const promise = Program.at(
programAddress,
new Provider(new Connection(url), new NodeWallet(Keypair.generate()), {})
)
.then(program => {
const programId = new PublicKey(programAddress);
const promise = Program.fetchIdl<Idl>(programId, getProvider(url))
.then(idl => {
if (!idl) {
throw new Error(`IDL not found for program: ${programAddress.toString()}`);
}

cachedAnchorProgramPromises[key] = {
__type: 'result',
result: program,
result: idl,
};
})
.catch(_ => {
Expand All @@ -36,6 +110,21 @@ export function useAnchorProgram(programAddress: string, url: string): Program |
return cacheEntry.result;
}

export function useAnchorProgram(programAddress: string, url: string): { program: Program | null; idl: Idl | null } {
const idlFromBinary = useIdlFromSolanaProgramBinary(programAddress);
const idlFromAnchorProgram = useIdlFromAnchorProgramSeed(programAddress, url);
const idl = idlFromBinary ?? idlFromAnchorProgram;
const program: Program<Idl> | null = useMemo(() => {
if (!idl) return null;
try {
return new Program(idl, new PublicKey(programAddress), getProvider(url));
} catch (e) {
return null;
}
}, [idl, programAddress, url]);
return { idl, program };
}

export type AnchorAccount = {
layout: string;
account: object;
Expand Down
2 changes: 1 addition & 1 deletion app/utils/anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function AnchorProgramName({
url: string;
defaultName?: string;
}) {
const program = useAnchorProgram(programId.toString(), url);
const { program } = useAnchorProgram(programId.toString(), url);
const programName = getAnchorProgramName(program) || defaultName;
return <>{programName}</>;
}
Expand Down
4 changes: 4 additions & 0 deletions app/utils/types/elfy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'elfy' {
const elfy: any;
export = elfy;
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@
"chart.js": "^4.3.0",
"classnames": "^2.3.1",
"cross-fetch": "^3.1.5",
"elfy": "^1.0.0",
"eslint": "8.39.0",
"eslint-config-next": "13.4.0",
"humanize-duration-ts": "^2.1.1",
"moment": "^2.29.4",
"next": "13.4.0",
"p-limit": "^3.1.0",
"pako": "^2.1.0",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
"react-content-loader": "^6.1.0",
Expand Down Expand Up @@ -68,6 +70,7 @@
"@types/bs58": "4.0.1",
"@types/chart.js": "^2.9.34",
"@types/node": "18.16.3",
"@types/pako": "^2.0.3",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.1",
"@types/react-select": "3.1.2",
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading