Skip to content

Commit

Permalink
Merge branch 'master' into pyusd-usdp-logo-support
Browse files Browse the repository at this point in the history
  • Loading branch information
catalinred authored Jun 21, 2024
2 parents 12f447d + 8f9939d commit 59d270f
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 84 deletions.
10 changes: 5 additions & 5 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,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 @@ -670,13 +670,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 @@ -691,7 +691,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
1 change: 0 additions & 1 deletion app/components/account/CompressedNftCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export function CompressedNftCard({ account }: { account: Account }) {
export function CompressedNftAccountHeader({ account }: { account: Account }) {
const { url } = useCluster();
const compressedNft = useCompressedNft({ address: account.pubkey.toString(), url });
if (!compressedNft) throw new Error('Compressed NFT not found');

if (compressedNft) {
return (
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
80 changes: 17 additions & 63 deletions app/providers/compressed-nft.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,14 @@
type CacheType<P> = Record<
string,
void | { __type: 'promise'; promise: Promise<void> } | { __type: 'result'; result: P }
>;
import useSWRImmutable from 'swr/immutable';

const cachedNftPromises: CacheType<CompressedNft | null> = {};

const cachedPromises = {
compressedNft: {} as CacheType<CompressedNft | null>,
compressedNftProof: {} as CacheType<CompressedNftProof | null>,
nftMetadataJson: {} as CacheType<any>,
};

function makeCache<Params, CacheValueType>(
cacheName: keyof typeof cachedPromises,
keygen: (params: Params) => string,
action: (params: Params) => Promise<CacheValueType>
): (params: Params) => CacheValueType {
return (params: Params) => {
const key = keygen(params);
const cacheEntry = cachedPromises[cacheName][key];

if (cacheEntry === undefined) {
const promise = action(params)
.then((value: CacheValueType) => {
cachedPromises[cacheName][key] = {
__type: 'result',
result: value,
};
})
.catch(_ => {
cachedNftPromises[key] = { __type: 'result', result: null };
});
cachedPromises[cacheName][key] = {
__type: 'promise',
promise,
};
throw promise;
} else if (cacheEntry.__type === 'promise') {
throw cacheEntry.promise;
}
return cacheEntry.result;
};
}

export const useMetadataJsonLink = makeCache<string, any>(
'nftMetadataJson',
(url: string) => url,
async (url: string) => {
export function useMetadataJsonLink(url: string) {
const { data, error } = useSWRImmutable(url, async (url: string) => {
return fetch(url).then(response => response.json());
}
);
});
return error ? null : data;
}

export const useCompressedNft = makeCache<{ address: string; url: string }, CompressedNft | null>(
'compressedNft',
({ address, url }) => `${address}-${url}`,
async ({ address, url }) => {
export function useCompressedNft({ address, url }: { address: string; url: string }): CompressedNft | null {
const { data, error } = useSWRImmutable([address, url], async ([address, url]): Promise<CompressedNft | null> => {
return fetch(`${url}`, {
body: JSON.stringify({
id: address,
Expand All @@ -72,18 +26,17 @@ export const useCompressedNft = makeCache<{ address: string; url: string }, Comp
.then(response => response.json())
.then((response: DasApiResponse<CompressedNft>) => {
if ('error' in response) {
throw new Error(response.error.message);
return null;
}

return response.result;
});
}
);
});
return error ? null : data ?? null;
}

export const useCompressedNftProof = makeCache<{ address: string; url: string }, CompressedNftProof | null>(
'compressedNftProof',
({ address, url }) => `proof-${address}-${url}`,
async ({ address, url }) => {
export function useCompressedNftProof({ address, url }: { address: string; url: string }): CompressedNftProof | null {
const { data, error } = useSWRImmutable([address, url], async ([address, url]) => {
return fetch(`${url}`, {
body: JSON.stringify({
id: address,
Expand All @@ -103,8 +56,9 @@ export const useCompressedNftProof = makeCache<{ address: string; url: string },

return response.result;
});
}
);
});
return error ? null : data ?? null;
}

type DasResponseTypes = CompressedNft | CompressedNftProof;
export type DasApiResponse<T extends DasResponseTypes> =
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
Loading

0 comments on commit 59d270f

Please sign in to comment.