diff --git a/app/components/instruction/ed25519/Ed25519DetailsCard.tsx b/app/components/instruction/ed25519/Ed25519DetailsCard.tsx index 0513215f1..81e005ebd 100644 --- a/app/components/instruction/ed25519/Ed25519DetailsCard.tsx +++ b/app/components/instruction/ed25519/Ed25519DetailsCard.tsx @@ -1,3 +1,6 @@ +import { Program } from '@coral-xyz/anchor'; +import { IdlType } from '@coral-xyz/anchor/dist/cjs/idl'; +import { sha256 } from '@noble/hashes/sha256'; import { ParsedTransaction, PartiallyDecodedInstruction, @@ -6,7 +9,11 @@ import { TransactionInstruction, } from '@solana/web3.js'; import bs58 from 'bs58'; -import React from 'react'; +import React, { useMemo } from 'react'; + +import { useAnchorProgram } from '@/app/providers/anchor'; +import { useCluster } from '@/app/providers/cluster'; +import { mapField } from '@/app/utils/anchor'; import { Address } from '../../common/Address'; import { Copyable } from '../../common/Copyable'; @@ -77,6 +84,178 @@ const extractData = ( } }; +function decodeMessageFromAnchorProgram( + anchorProgram: Program, + message: Uint8Array +): { name: string; data: any } | null { + const messageDisc = Buffer.from(message.slice(0, 8)).toString('hex'); + const coder = anchorProgram.coder.types; + for (const [_, typeLayouts] of Object.entries(anchorProgram.coder.types)) { + for (const [name] of typeLayouts.entries()) { + try { + const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); + const disc = Buffer.from(sha256(`global:${capitalizedName}`).slice(0, 8)).toString('hex'); + + if (disc === messageDisc) { + const decoded = coder.decode(name, Buffer.from(message.slice(8))); + if (decoded) { + return { data: decoded, name }; + } + } + } catch (e) { + console.log('Error decoding message with anchor program', e); + } + } + } + return null; +} + +function SignatureDetails({ + index, + offset, + signature, + pubkey, + message, + messageIx, +}: { + index: number; + offset: Ed25519SignatureOffsets; + signature: Uint8Array | null; + pubkey: Uint8Array | null; + message: Uint8Array; + messageIx: PartiallyDecodedInstruction; +}) { + const { url } = useCluster(); + const anchorProgram = useAnchorProgram(messageIx.programId.toBase58(), url); + + const decodedMessage = useMemo(() => { + if (!anchorProgram?.idl || !anchorProgram?.program) { + return null; + } + return decodeMessageFromAnchorProgram(anchorProgram.program, Buffer.from(message.toString(), 'hex')); + }, [anchorProgram, message]); + + const messageRow = useMemo(() => { + if (!decodedMessage || !anchorProgram?.idl || !anchorProgram?.program) { + return ( + + Message + + + {Buffer.from(message).toString('base64')} + + + + ); + } + + const name = decodedMessage.name; + const data = decodedMessage.data; + + if (!name || !data) { + return null; + } + + const type: IdlType = { defined: { name } }; + + return ( + + + + Message Payload + + + Payload Type + + + Value + + + {mapField(name, data, type, anchorProgram.program.idl, 'sigverify-message', 0)} + + ); + }, [decodedMessage, message, anchorProgram?.idl, anchorProgram?.program]); + + return ( + + + + Signature #{index + 1} + + + + Signature Reference + + Instruction {offset.signatureInstructionIndex}, Offset {offset.signatureOffset} + + + + Signature + + {signature ? ( + + {Buffer.from(signature).toString('base64')} + + ) : ( + Invalid Reference + )} + + + + Public Key Reference + + Instruction {offset.publicKeyInstructionIndex}, Offset {offset.publicKeyOffset} + + + + Public Key + + {pubkey ? ( +
+ ) : ( + Invalid Reference + )} + + + + Message Reference + + Instruction {offset.messageInstructionIndex}, Offset {offset.messageDataOffset}, Size{' '} + {offset.messageDataSize} + + + + Message Program + +
+ + + {messageRow} + + ); +} + export function Ed25519DetailsCard(props: DetailsProps) { const { tx, ix, index, result, innerCards, childIndex } = props; @@ -97,7 +276,6 @@ export function Ed25519DetailsCard(props: DetailsProps) {
- {offsets.map((offset, index) => { const signature = extractData( tx, @@ -109,100 +287,24 @@ export function Ed25519DetailsCard(props: DetailsProps) { const pubkey = extractData(tx, offset.publicKeyInstructionIndex, ix.data, offset.publicKeyOffset, 32); - const message = extractData( - tx, - offset.messageInstructionIndex, - ix.data, - offset.messageDataOffset, - offset.messageDataSize - ); + const messageIx = tx.message.instructions[ + offset.messageInstructionIndex + ] as PartiallyDecodedInstruction; + + const message = bs58 + .decode(messageIx.data) + .slice(offset.messageDataOffset, offset.messageDataOffset + offset.messageDataSize); return ( - - - - Signature #{index + 1} - - - - Signature Reference - - {offset.signatureInstructionIndex === ED25519_SELF_REFERENCE_INSTRUCTION_INDEX - ? 'This instruction' - : `Instruction ${offset.signatureInstructionIndex}`} - {', '} - Offset {offset.signatureOffset} - - - - Signature - - {signature ? ( - - - {Buffer.from(signature).toString('base64')} - - - ) : ( - 'Invalid reference' - )} - - - - Public Key Reference - - {offset.publicKeyInstructionIndex === ED25519_SELF_REFERENCE_INSTRUCTION_INDEX - ? 'This instruction' - : `Instruction ${offset.publicKeyInstructionIndex}`} - {', '} - Offset {offset.publicKeyOffset} - - - - Public Key - - {pubkey ? ( -
- ) : ( - 'Invalid reference' - )} - - - - Message Reference - - {offset.messageInstructionIndex === ED25519_SELF_REFERENCE_INSTRUCTION_INDEX - ? 'This instruction' - : `Instruction ${offset.messageInstructionIndex}`} - {', '} - Offset {offset.messageDataOffset}, Size {offset.messageDataSize} - - - - Message - - {message ? ( - - - {Buffer.from(message).toString('base64')} - - - ) : ( - 'Invalid reference' - )} - - - + ); })} diff --git a/app/providers/anchor.tsx b/app/providers/anchor.tsx index 1148fb10a..32bb9cbfe 100644 --- a/app/providers/anchor.tsx +++ b/app/providers/anchor.tsx @@ -4,15 +4,11 @@ import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import * as elfy from 'elfy'; import pako from 'pako'; import { useEffect, useMemo } from 'react'; +import useSWR from 'swr'; import { formatIdl } from '../utils/convertLegacyIdl'; import { useAccountInfo, useFetchAccountInfo } from './accounts'; -const cachedAnchorProgramPromises: Record< - string, - void | { __type: 'promise'; promise: Promise } | { __type: 'result'; result: Idl | null } -> = {}; - // eslint-disable-next-line @typescript-eslint/no-unused-vars function useIdlFromSolanaProgramBinary(programAddress: string): Idl | null { const fetchAccountInfo = useFetchAccountInfo(); @@ -81,53 +77,46 @@ function getProvider(url: string) { return new AnchorProvider(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 programId = new PublicKey(programAddress); - const promise = Program.fetchIdl(programId, getProvider(url)) - .then(idl => { - if (!idl) { - throw new Error(`IDL not found for program: ${programAddress.toString()}`); - } - - cachedAnchorProgramPromises[key] = { - __type: 'result', - result: idl, - }; - }) - .catch(_ => { - cachedAnchorProgramPromises[key] = { __type: 'result', result: null }; - }); - cachedAnchorProgramPromises[key] = { - __type: 'promise', - promise, - }; - throw promise; - } else if (cacheEntry.__type === 'promise') { - throw cacheEntry.promise; +function getProgram(idl: Idl, programAddress: string, url: string) { + const provider = getProvider(url); + + try { + try { + // Try using the uploaded IDL + return new Program(idl, provider); + } catch (e) { + // If raw IDL fails, try with formatted IDL + try { + const unprunedIdl = formatIdl(idl, programAddress, false); + return new Program(unprunedIdl, provider); + } catch (e) { + // Try again with types removed + const prunedIdl = formatIdl(idl, programAddress, true); + return new Program(prunedIdl, provider); + } + } + } catch (e) { + console.error('Error creating anchor program for', programAddress, e, { idl }); + return null; } - return cacheEntry.result; } export function useAnchorProgram(programAddress: string, url: string): { program: Program | null; idl: Idl | null } { - // TODO(ngundotra): Rewrite this to be more efficient - // const idlFromBinary = useIdlFromSolanaProgramBinary(programAddress); - const idlFromAnchorProgram = useIdlFromAnchorProgramSeed(programAddress, url); - const idl = idlFromAnchorProgram; - const program: Program | null = useMemo(() => { - if (!idl) return null; + const { data } = useSWR([programAddress, url], async () => { try { - const program = new Program(formatIdl(idl, programAddress), getProvider(url)); - return program; + const programId = new PublicKey(programAddress); + const idl = await Program.fetchIdl(programId, getProvider(url)); + if (!idl) { + throw new Error(`IDL not found for program: ${programAddress}`); + } + return { idl, program: getProgram(idl, programAddress, url) }; } catch (e) { - console.error('Error creating anchor program for', programAddress, e, { idl }); + console.error('Error fetching IDL:', e); return null; } - }, [idl, programAddress, url]); - return { idl, program }; + }); + + return data ?? { idl: null, program: null }; } export type AnchorAccount = { diff --git a/app/utils/anchor.tsx b/app/utils/anchor.tsx index 504f363a0..6ffacfd9a 100644 --- a/app/utils/anchor.tsx +++ b/app/utils/anchor.tsx @@ -155,7 +155,14 @@ export function mapAccountToRows(accountData: any, accountType: IdlTypeDef, idl: }); } -function mapField(key: string, value: any, type: IdlType, idl: Idl, keySuffix?: any, nestingLevel = 0): ReactNode { +export function mapField( + key: string, + value: any, + type: IdlType, + idl: Idl, + keySuffix?: any, + nestingLevel = 0 +): ReactNode { let itemKey = key; if (/^-?\d+$/.test(keySuffix)) { itemKey = `#${keySuffix}`; diff --git a/app/utils/convertLegacyIdl.ts b/app/utils/convertLegacyIdl.ts index b930b5dcd..ee688774a 100644 --- a/app/utils/convertLegacyIdl.ts +++ b/app/utils/convertLegacyIdl.ts @@ -192,8 +192,8 @@ function traverseIdlFields(fields: IdlDefinedFields, refs: Set) { typeof field === 'string' ? traverseType(field, refs) : typeof field === 'object' && 'type' in field - ? traverseType(field.type, refs) - : traverseType(field, refs) + ? traverseType(field.type, refs) + : traverseType(field, refs) ); } @@ -436,14 +436,19 @@ export function getIdlSpecType(idl: any): IdlSpec { export type IdlSpec = '0.1.0' | 'legacy'; -export function formatIdl(idl: any, programAddress?: string): Idl { +export function formatIdl(idl: any, programAddress?: string, removeTypes = true): Idl { const spec = getIdlSpecType(idl); switch (spec) { case '0.1.0': return idl as Idl; - case 'legacy': - return removeUnusedTypes(convertLegacyIdl(idl as LegacyIdl, programAddress)); + case 'legacy': { + let converted = convertLegacyIdl(idl as LegacyIdl, programAddress); + if (removeTypes) { + converted = removeUnusedTypes(converted); + } + return converted; + } default: throw new Error(`IDL spec not supported: ${spec}`); }