UNPKG

@solana-developers/helpers

Version:
248 lines 10.4 kB
import { Keypair, } from "@solana/web3.js"; import { Program, AnchorProvider, EventParser, BorshAccountsCoder, BorshInstructionCoder, Wallet, } from "@coral-xyz/anchor"; import BN from "bn.js"; import { formatIdl } from "./convertLegacyIdl.js"; /** * Loads an Anchor IDL from a local file path * * @param idlPath - Path to the IDL JSON file * @returns The parsed IDL * * @example * ```typescript * const idl = await getIdlByPath("./idl/program.json"); * ``` */ export async function getIdlByPath(idlPath) { const fs = await import("node:fs"); const path = await import("node:path"); // Load and parse IDL file const idlFile = fs.readFileSync(path.resolve(idlPath), "utf8"); const idl = JSON.parse(idlFile); return idl; } /** * Fetches an Anchor IDL from a program on-chain * * @param programId - Public key of the program * @param provider - Anchor Provider instance (you can use createProviderForConnection to create) * @returns The fetched IDL * @throws If IDL cannot be found for the program * * @example * ```typescript * const idl = await getIdlByProgramId( * new PublicKey("Foo1111111111111111111111111111111111111"), * connection * ); * ``` */ export async function getIdlByProgramId(programId, provider) { var idl = await Program.fetchIdl(programId, provider); if (!idl) throw new Error(`IDL not found for program ${programId.toString()}`); return idl; } /** * Creates an Anchor provider for a given connection * * @param connection - The Solana connection object * @param keypair - Optional keypair to use for the provider (defaults to a new random keypair) * @param options - Optional configuration options for the provider * @returns An Anchor provider instance * * @example * ```typescript * const provider = createProviderForConnection(connection); * ``` */ export function createProviderForConnection(connection, keypair = new Keypair(), options = { commitment: "confirmed", }) { return new AnchorProvider(connection, new Wallet(keypair), options); } /** * Fetches and parses an account's data using an Anchor IDL * * @param idl - The Anchor IDL (use getIdlByProgramId or getIdlByPath to obtain) * @param accountName - The name of the account as defined in the IDL * @param accountAddress - The public key of the account to fetch * @param provider - Anchor Provider instance (you can use createProviderForConnection to create) * @param programId - Optional program ID needed for legacy IDLs * @returns The decoded account data * * @example * ```typescript * const idl = await getIdlByProgramId(programId, connection); * const data = await getIdlParsedAccountData(idl, "counter", accountAddress); * ``` */ export async function getIdlParsedAccountData(idl, accountName, accountAddress, provider, programId) { const program = new Program(formatIdl(idl, programId?.toString()), provider); const accountInfo = await provider.connection.getAccountInfo(accountAddress); if (!accountInfo) { throw new Error(`Account ${accountAddress.toString()} not found`); } return program.coder.accounts.decode(accountName, accountInfo.data); } /** * Parses Anchor events from a transaction * * @param idl - The Anchor IDL (use getIdlByProgramId or getIdlByPath to obtain) * @param signature - Transaction signature to parse events from * @param provider - Anchor Provider instance (you can use createProviderForConnection to create) * @param programId - Optional program ID needed for legacy IDLs * @returns Array of parsed events with their name and data * * @example * ```typescript * const idl = await getIdlByPath("./idl/program.json"); * const events = await parseAnchorTransactionEvents(idl, signature); * ``` */ export async function parseAnchorTransactionEvents(idl, signature, provider, programId) { const program = new Program(formatIdl(idl, programId?.toString()), provider); const parser = new EventParser(program.programId, program.coder); const transaction = await provider.connection.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (!transaction?.meta?.logMessages) { return []; } const events = []; for (const event of parser.parseLogs(transaction.meta.logMessages)) { events.push({ name: event.name, data: event.data, }); } return events; } /** * Decodes all Anchor instructions and their involved accounts in a transaction * * @param idl - The Anchor IDL (use getIdlByProgramId or getIdlByPath to obtain) * @param signature - Transaction signature to decode * @param connection - Optional connection object (uses default provider if not specified) * @param programId - Optional program ID needed for legacy IDLs * @returns Decoded transaction with instructions and accounts * * @example * ```typescript * const idl = await getIdlByProgramId(programId, connection); * const decoded = await decodeAnchorTransaction(idl, signature); * ``` */ export async function decodeAnchorTransaction(idl, signature, provider, programId) { const program = new Program(formatIdl(idl, programId?.toString()), provider); const accountsCoder = new BorshAccountsCoder(program.idl); const instructionCoder = new BorshInstructionCoder(program.idl); const transaction = await provider.connection.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (!transaction) { throw new Error(`Transaction ${signature} not found`); } const decodedInstructions = []; const message = transaction.transaction.message; const instructions = "addressTableLookups" in message ? message.compiledInstructions : message.instructions; const accountKeys = message.getAccountKeys(); for (const ix of instructions) { const programId = accountKeys.get("programIdIndex" in ix ? ix.programIdIndex : ix.programId); if (!programId) continue; if (programId.equals(program.programId)) { try { const decoded = instructionCoder.decode(Buffer.from(ix.data)); if (decoded) { const ixType = idl.instructions.find((i) => i.name === decoded.name); const accountIndices = "accounts" in ix ? ix.accounts : ix.accountKeyIndexes; // Get all accounts involved in this instruction const accounts = await Promise.all(accountIndices.map(async (index, i) => { const pubkey = accountKeys.get(index); if (!pubkey) return null; const accountMeta = ixType?.accounts[i]; const accountInfo = await provider.connection.getAccountInfo(pubkey); let accountData; if (accountInfo?.owner.equals(program.programId)) { try { const accountType = idl.accounts?.find((acc) => accountInfo.data .slice(0, 8) .equals(accountsCoder.accountDiscriminator(acc.name.toLowerCase()))); if (accountType) { accountData = accountsCoder.decode(accountType.name.toLowerCase(), accountInfo.data); } } catch (e) { console.log(`Failed to decode account data: ${e}`); } } return { name: accountMeta?.name || `account_${i}`, pubkey: pubkey.toString(), isSigner: message.staticAccountKeys.findIndex((k) => k.equals(pubkey)) < message.header.numRequiredSignatures || false, isWritable: message.isAccountWritable(index), ...(accountData && { data: accountData }), }; })); decodedInstructions.push({ name: decoded.name, type: ixType ? JSON.stringify(ixType.args) : "unknown", data: decoded.data, accounts, toString: function () { let output = `\nInstruction: ${this.name}\n`; output += `├─ Arguments: ${JSON.stringify(formatData(this.data))}\n`; output += `└─ Accounts:\n`; this.accounts.forEach((acc) => { output += ` ├─ ${acc.name}:\n`; output += ` │ ├─ Address: ${acc.pubkey}\n`; output += ` │ ├─ Signer: ${acc.isSigner}\n`; output += ` │ ├─ Writable: ${acc.isWritable}\n`; if (acc.data) { output += ` │ └─ Data: ${JSON.stringify(formatData(acc.data))}\n`; } }); return output; }, }); } } catch (e) { console.log(`Failed to decode instruction: ${e}`); } } } return { instructions: decodedInstructions, toString: function () { let output = "\n=== Decoded Transaction ===\n"; this.instructions.forEach((ix, index) => { output += `\nInstruction ${index + 1}:${ix.toString()}`; }); return output; }, }; } // Helper function to format data function formatData(data) { if (data instanceof BN) { return `<BN: ${data.toString()}>`; } if (Array.isArray(data)) { return data.map(formatData); } if (typeof data === "object" && data !== null) { return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, formatData(v)])); } return data; } //# sourceMappingURL=idl.js.map