UNPKG

@debridge-finance/solana-transaction-parser

Version:

Tool for parsing arbitrary Solana transactions with IDL/custom parsers

256 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SolanaParser = void 0; const buffer_1 = require("buffer"); const web3_js_1 = require("@solana/web3.js"); const spl_token_1 = require("@solana/spl-token"); const anchor_1 = require("@coral-xyz/anchor"); const decoders_1 = require("./decoders"); const helpers_1 = require("./helpers"); const COMPUTE_BUDGET_PROGRAM_ID = new web3_js_1.PublicKey("ComputeBudget111111111111111111111111111111"); function flattenIdlAccounts(accounts, prefix) { return accounts .map((account) => { const accName = account.name; if ("accounts" in account) { const newPrefix = prefix ? `${prefix}.${accName}` : accName; return flattenIdlAccounts(account.accounts, newPrefix); } else { return { ...account, name: prefix ? `${prefix}.${accName}` : accName, }; } }) .flat(); } /** * Class for parsing arbitrary solana transactions in various formats * - by txHash * - from raw transaction data (base64 encoded or buffer) * - @solana/web3.js getTransaction().message object * - @solana/web3.js getParsedTransaction().message or Transaction.compileMessage() object * - @solana/web3.js TransactionInstruction object */ class SolanaParser { /** * Initializes parser object * `SystemProgram`, `TokenProgram` and `AssociatedTokenProgram` are supported by default * but may be overriden by providing custom idl/custom parser * @param programInfos list of objects which contains programId and corresponding idl * @param parsers list of pairs (programId, custom parser) */ constructor(programInfos, parsers) { this.idlParsers = new Map(); const standartParsers = [ [web3_js_1.SystemProgram.programId.toBase58(), decoders_1.decodeSystemInstruction], [spl_token_1.TOKEN_PROGRAM_ID.toBase58(), decoders_1.decodeTokenInstruction], [spl_token_1.TOKEN_2022_PROGRAM_ID.toBase58(), decoders_1.decodeToken2022Instruction], [spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID.toBase58(), decoders_1.decodeAssociatedTokenInstruction], [COMPUTE_BUDGET_PROGRAM_ID.toBase58(), decoders_1.decodeComputeBudgetInstruction], ]; let result; parsers = parsers || []; for (const programInfo of programInfos) { parsers.push(this.buildIdlParser(new web3_js_1.PublicKey(programInfo.programId), programInfo.idl)); } if (!parsers) { result = new Map(standartParsers); } else { // first set provided parsers result = new Map(parsers); // append standart parsers if parser not exist yet for (const parserInfo of standartParsers) { if (!result.has(parserInfo[0])) { result.set(...parserInfo); } } } this.instructionParsers = result; } /** * Adds (or updates) parser for provided programId * @param programId program id to add parser for * @param parser parser to parse programId instructions */ addParser(programId, parser) { this.instructionParsers.set(programId.toBase58(), parser); // Remove idlParser cache if present (custom parser overrides IDL-based one) this.idlParsers.delete(programId.toBase58()); } /** * Adds (or updates) parser for provided programId * @param programId program id to add parser for * @param idl IDL that describes anchor program */ addParserFromIdl(programId, idl) { this.instructionParsers.set(...this.buildIdlParser(new web3_js_1.PublicKey(programId), idl)); } buildIdlParser(programId, idl) { const pubkey = programId.toBase58(); if (!this.idlParsers.has(pubkey)) { this.idlParsers.set(pubkey, { instructionCoder: new anchor_1.BorshInstructionCoder(idl), eventCoder: new anchor_1.BorshEventCoder(idl), }); } const idlParser = (instruction) => { const programParser = this.idlParsers.get(pubkey); if (!programParser) { return this.buildUnknownParsedInstruction(instruction.programId, instruction.keys, instruction.data); } const { instructionCoder, eventCoder } = programParser; const parsedIx = instructionCoder.decode(instruction.data); if (!parsedIx) { if (instruction.data && instruction.data.length > 8) { const eventData = anchor_1.utils.bytes.base64.encode(instruction.data.subarray(8)); const parsedEvent = eventCoder.decode(eventData); if (parsedEvent) { return { name: parsedEvent.name, programId: instruction.programId, args: parsedEvent.data, accounts: instruction.keys.map((meta, idx) => ({ name: `Account ${idx}`, ...meta, })), }; } } return this.buildUnknownParsedInstruction(instruction.programId, instruction.keys, instruction.data); } const ix = idl.instructions.find((instr) => instr.name === parsedIx.name); if (!ix) { return this.buildUnknownParsedInstruction(instruction.programId, instruction.keys, instruction.data, parsedIx.name); } const flatIdlAccounts = flattenIdlAccounts(ix.accounts); const accounts = instruction.keys.map((meta, idx) => { if (idx < flatIdlAccounts.length) { return { name: flatIdlAccounts[idx].name, ...meta, }; } // "Remaining accounts" are unnamed in Anchor. else { return { name: `Remaining ${idx - flatIdlAccounts.length}`, ...meta, }; } }); return { name: parsedIx.name, accounts: accounts, programId: instruction.programId, args: parsedIx.data, }; }; return [pubkey, idlParser.bind(this)]; } /** * Removes parser for provided program id * @param programId program id to remove parser for */ removeParser(programId) { this.instructionParsers.delete(programId.toBase58()); this.idlParsers.delete(programId.toBase58()); } buildUnknownParsedInstruction(programId, accounts, argData, name) { return { programId, accounts, args: { unknown: argData }, name: name || "unknown", }; } /** * Parses instruction * @param instruction transaction instruction to parse * @returns parsed transaction instruction or UnknownInstruction */ parseInstruction(instruction) { if (!this.instructionParsers.has(instruction.programId.toBase58())) { return this.buildUnknownParsedInstruction(instruction.programId, instruction.keys, instruction.data); } else { const parser = this.instructionParsers.get(instruction.programId.toBase58()); return parser(instruction); } } /** * Parses transaction data * @param txMessage message to parse * @param altLoadedAddresses VersionedTransaction.meta.loaddedAddresses if tx is versioned * @returns list of parsed instructions */ parseTransactionData(txMessage, altLoadedAddresses = undefined) { const parsedAccounts = (0, helpers_1.parseTransactionAccounts)(txMessage, altLoadedAddresses); return txMessage.compiledInstructions.map((instruction) => this.parseInstruction((0, helpers_1.compiledInstructionToInstruction)(instruction, parsedAccounts))); } /** * Parses transaction data retrieved from Connection.getParsedTransaction * @param txParsedMessage message to parse * @returns list of parsed instructions */ parseTransactionParsedData(txParsedMessage) { const parsedAccounts = txParsedMessage.accountKeys.map((metaLike) => ({ isSigner: metaLike.signer, isWritable: metaLike.writable, pubkey: metaLike.pubkey, })); return txParsedMessage.instructions.map((parsedIx) => this.parseInstruction((0, helpers_1.parsedInstructionToInstruction)(parsedIx, parsedAccounts))); } /** * Fetches tx from blockchain and parses it * @param connection web3 Connection * @param txId transaction id * @param flatten - true if CPI calls need to be parsed too * @returns list of parsed instructions */ async parseTransactionByHash(connection, txId, flatten = false, commitment = "confirmed") { const transaction = await connection.getTransaction(txId, { commitment: commitment, maxSupportedTransactionVersion: 0 }); if (!transaction) return null; if (flatten) { const flattened = (0, helpers_1.flattenTransactionResponse)(transaction); return flattened.map((ix) => this.parseInstruction(ix)); } return this.parseTransactionData(transaction.transaction.message, transaction.meta?.loadedAddresses); } /** * Parses transaction dump * @param txDump base64-encoded string or raw Buffer which contains tx dump * @returns list of parsed instructions */ async parseTransactionDump(connection, txDump) { if (!(txDump instanceof buffer_1.Buffer)) txDump = buffer_1.Buffer.from(txDump, "base64"); const vtx = web3_js_1.VersionedTransaction.deserialize(txDump); let loadedAddresses = { writable: [], readonly: [] }; if (vtx.version !== "legacy") { const accountsToFetch = vtx.message.addressTableLookups.map((alt) => alt.accountKey); if (accountsToFetch.length > 0) { const fetched = await connection.getMultipleAccountsInfo(accountsToFetch); const altAccounts = fetched .filter((f) => f !== null && f.data.length > 0) .map((f) => web3_js_1.AddressLookupTableAccount.deserialize(f.data)); const altWritableAccounts = []; const altReadonlyAccounts = []; vtx.message.addressTableLookups.map((compiledALT, idx) => { altWritableAccounts.push(...compiledALT.writableIndexes.map((writableIdx) => altAccounts[idx].addresses[writableIdx])); altReadonlyAccounts.push(...compiledALT.readonlyIndexes.map((writableIdx) => altAccounts[idx].addresses[writableIdx])); }); loadedAddresses = { readonly: altReadonlyAccounts, writable: altWritableAccounts, }; } } return this.parseTransactionData(vtx.message, loadedAddresses); } } exports.SolanaParser = SolanaParser; //# sourceMappingURL=parsers.js.map