diff --git a/README.md b/README.md index 217c12f..a63a580 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,6 @@ Error Handling & Utilities: [Get a Solana Explorer link for a transaction, address, or block](#get-a-solana-explorer-link-for-a-transaction-address-or-block) -Anchor Program Interaction: -[Parse account data with IDL](#parse-account-data-with-idl) - -[Parse transaction events](#parse-transaction-events) - -[Decode Anchor transaction](#decode-anchor-transaction) - ## Installation ```bash @@ -567,71 +560,3 @@ await prepareTransactionWithCompute( ``` Both functions help with common transaction handling tasks in Solana, making it easier to send reliable transactions with appropriate compute unit settings. - -## Anchor IDL Utilities - -### Parse Account Data with IDL - -Usage: - -```typescript -const accountData = await getIdlParsedAccountData( - "./idl/program.json", - "counter", - accountAddress, - connection, -); - -// Decoded Data: { count: } -``` - -Fetches and parses an account's data using an Anchor IDL file. This is useful when you need to decode account data from Anchor programs. - -### Parse Transaction Events - -Usage: - -```typescript -const events = await parseAnchorTransactionEvents( - "./idl/program.json", - signature, - connection, -); - -// Events will be an array of: -// { -// name: "GameCreated", -// data: { gameId: "123", player: "..." } -// } -``` - -Parses all Anchor events emitted in a transaction. This helps you track and verify program events after transaction execution. - -### Decode Anchor Transaction - -Usage: - -```typescript -const decoded = await decodeAnchorTransaction( - "./idl/program.json", - signature, - connection, -); - -// Print human-readable format -console.log(decoded.toString()); - -// Access specific instruction data -decoded.instructions.forEach((ix) => { - console.log(`Instruction: ${ix.name}`); - console.log(`Arguments: ${JSON.stringify(ix.data)}`); - console.log(`Accounts: ${ix.accounts.map((acc) => acc.name).join(", ")}`); -}); -``` - -Provides detailed decoding of all Anchor instructions in a transaction, including: - -- Instruction names and arguments -- All involved accounts with their roles (signer/writable) -- Account data for program-owned accounts -- Human-readable string representation diff --git a/package-lock.json b/package-lock.json index af3752c..ebbced4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "2.5.6", "license": "MIT", "dependencies": { - "@coral-xyz/anchor": "^0.30.1", "@solana/spl-token": "^0.4.8", "@solana/spl-token-metadata": "^0.1.4", "@solana/web3.js": "^1.98.0", @@ -35,80 +34,6 @@ "node": ">=6.9.0" } }, - "node_modules/@coral-xyz/anchor": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.30.1.tgz", - "integrity": "sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ==", - "dependencies": { - "@coral-xyz/anchor-errors": "^0.30.1", - "@coral-xyz/borsh": "^0.30.1", - "@noble/hashes": "^1.3.1", - "@solana/web3.js": "^1.68.0", - "bn.js": "^5.1.2", - "bs58": "^4.0.1", - "buffer-layout": "^1.2.2", - "camelcase": "^6.3.0", - "cross-fetch": "^3.1.5", - "crypto-hash": "^1.3.0", - "eventemitter3": "^4.0.7", - "pako": "^2.0.3", - "snake-case": "^3.0.4", - "superstruct": "^0.15.4", - "toml": "^3.0.0" - }, - "engines": { - "node": ">=11" - } - }, - "node_modules/@coral-xyz/anchor-errors": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz", - "integrity": "sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@coral-xyz/anchor/node_modules/base-x": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", - "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/@coral-xyz/anchor/node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/@coral-xyz/anchor/node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/@coral-xyz/anchor/node_modules/superstruct": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", - "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" - }, - "node_modules/@coral-xyz/borsh": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.30.1.tgz", - "integrity": "sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ==", - "dependencies": { - "bn.js": "^5.1.2", - "buffer-layout": "^1.2.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@solana/web3.js": "^1.68.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1038,14 +963,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/buffer-layout": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz", - "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==", - "engines": { - "node": ">=4.5" - } - }, "node_modules/bufferutil": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", @@ -1059,17 +976,6 @@ "node": ">=6.14.2" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1088,25 +994,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/crypto-hash": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-1.3.0.tgz", - "integrity": "sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -1144,15 +1031,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -1363,28 +1241,11 @@ "node": "*" } }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1415,11 +1276,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, "node_modules/prettier": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", @@ -1512,15 +1368,6 @@ } ] }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", @@ -1541,11 +1388,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index d6538dc..0d5ac4d 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,7 @@ "@solana/spl-token-metadata": "^0.1.4", "@solana/web3.js": "^1.98.0", "bs58": "^6.0.0", - "dotenv": "^16.4.5", - "@coral-xyz/anchor": "^0.30.1" + "dotenv": "^16.4.5" }, "devDependencies": { "@types/node": "^20.16.1", diff --git a/src/lib/transaction.ts b/src/lib/transaction.ts index cc648ed..6f350a3 100644 --- a/src/lib/transaction.ts +++ b/src/lib/transaction.ts @@ -10,22 +10,8 @@ import { TransactionInstruction, TransactionMessage, VersionedTransaction, - Message, - MessageV0, - MessageCompiledInstruction, } from "@solana/web3.js"; import { getErrorFromRPCResponse } from "./logs"; -import { - Program, - Idl, - AnchorProvider, - EventParser, - BorshAccountsCoder, - BorshInstructionCoder, - BN, -} from "@coral-xyz/anchor"; -import * as fs from "fs"; -import * as path from "path"; export const confirmTransaction = async ( connection: Connection, @@ -355,272 +341,3 @@ export async function prepareTransactionWithCompute( }), ); } - -/** - * Fetches and parses an account's data using an Anchor IDL file - * - * @param idlPath - Path to the IDL JSON file - * @param accountName - The name of the account as defined in the IDL - * @param accountAddress - The public key of the account to fetch - * @param connection - Optional connection object (uses default provider if not specified) - * @returns The decoded account data - * - * @throws If the IDL file doesn't exist or account cannot be decoded - */ -export async function getIdlParsedAccountData( - idlPath: string, - accountName: string, - accountAddress: PublicKey, - connection?: Connection, -): Promise { - // Load and parse IDL file - const idlFile = fs.readFileSync(path.resolve(idlPath), "utf8"); - const idl = JSON.parse(idlFile) as Idl; - - // Get or create provider - const provider = connection - ? new AnchorProvider(connection, AnchorProvider.env().wallet, {}) - : AnchorProvider.env(); - - // Create program - const program = new Program(idl, 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) as T; -} - -/** - * Parses Anchor events from a transaction - * - * @param idlPath - Path to the IDL JSON file - * @param signature - Transaction signature to parse events from - * @param connection - Optional connection object (uses default provider if not specified) - * @returns Array of parsed events with their name and data - */ -export async function parseAnchorTransactionEvents( - idlPath: string, - signature: string, - connection?: Connection, -): Promise< - Array<{ - name: string; - data: any; - }> -> { - const idlFile = fs.readFileSync(path.resolve(idlPath), "utf8"); - const idl = JSON.parse(idlFile) as Idl; - - const provider = connection - ? new AnchorProvider(connection, AnchorProvider.env().wallet, {}) - : AnchorProvider.env(); - - const program = new Program(idl, provider); - const parser = new EventParser(program.programId, program.coder); - - const transaction = await provider.connection.getTransaction(signature, { - commitment: "confirmed", - }); - - if (!transaction?.meta?.logMessages) { - return []; - } - - const events: Array<{ name: string; data: any }> = []; - for (const event of parser.parseLogs(transaction.meta.logMessages)) { - events.push({ - name: event.name, - data: event.data, - }); - } - - return events; -} - -/** - * Account involved in an instruction - */ -type InvolvedAccount = { - name: string; - pubkey: string; - isSigner: boolean; - isWritable: boolean; - data?: Record; // Decoded account data if it's a program account -}; - -/** - * Decoded Anchor instruction with all involved accounts - */ -export type DecodedAnchorInstruction = { - name: string; - type: string; - data: Record; - accounts: InvolvedAccount[]; - toString: () => string; -}; - -/** - * Decoded Anchor transaction containing all instructions and their accounts - */ -export type DecodedTransaction = { - instructions: DecodedAnchorInstruction[]; - toString: () => string; -}; - -/** - * Decodes all Anchor instructions and their involved accounts in a transaction - */ -export async function decodeAnchorTransaction( - idlPath: string, - signature: string, - connection?: Connection, -): Promise { - const idlFile = fs.readFileSync(path.resolve(idlPath), "utf8"); - const idl = JSON.parse(idlFile) as Idl; - - const provider = connection - ? new AnchorProvider(connection, AnchorProvider.env().wallet, {}) - : AnchorProvider.env(); - - const program = new Program(idl, provider); - const accountsCoder = new BorshAccountsCoder(idl); - const instructionCoder = new BorshInstructionCoder(idl); - - const transaction = await provider.connection.getTransaction(signature, { - commitment: "confirmed", - maxSupportedTransactionVersion: 0, - }); - - if (!transaction) { - throw new Error(`Transaction ${signature} not found`); - } - - const decodedInstructions: DecodedAnchorInstruction[] = []; - - // Decode instructions - const message = transaction.transaction.message; - const instructions = - "version" in message - ? message.compiledInstructions - : (message as Message).instructions; - const accountKeys = message.getAccountKeys(); - - for (const ix of instructions) { - const programId = accountKeys.get( - "programIdIndex" in ix - ? (ix as MessageCompiledInstruction).programIdIndex - : (ix as any).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: InvolvedAccount[] = 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)), - ); - if (accountType) { - accountData = accountsCoder.decode( - accountType.name, - 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 (this: DecodedTransaction) { - 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: any): any { - if (data instanceof BN) { - return ``; - } - 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; -}