@solana-developers/helpers
Version:
Solana helper functions
248 lines • 10.4 kB
JavaScript
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