Summary
@solana/transaction-introspection turns a getTransaction RPC response into
a list of instructions with real addresses. It resolves account indices
(including lookup-table addresses), decodes inner CPI instructions, and walks
the full instruction tree in explorer order.
Looking for how a program inspects sibling instructions on-chain at runtime? That is the Instructions sysvar - see Instruction Introspection. This page is about decoding a transaction off-chain from an RPC response.
A getTransaction response does not hand you a
ready-to-use list of instructions. Accounts are stored as numeric indices,
versioned transactions split
those accounts into static keys plus addresses loaded from
Address Lookup Tables, and
inner instructions from cross-program invocations (CPIs) arrive as encoded blobs
in the transaction metadata.
@solana/transaction-introspection does this resolution for you. It decodes the
wire transaction, maps every account index back to its Address, normalizes the
inner instructions, and returns instructions in the same outer-then-inner order
a block explorer shows. The returned instructions plug directly into the
codama-generated clients' (e.g., @solana-program/* ) parseXInstruction
helpers.
npm install @solana/transaction-introspection @solana/kit @solana/instructions @solana-program/token
Decode the response
Fetch the transaction with a wire-format encoding and pass the response to
decodeTransactionFromRpcResponse.
import { createSolanaRpc, signature } from "@solana/kit";import { decodeTransactionFromRpcResponse } from "@solana/transaction-introspection";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const txid ="3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX";const rpcTx = await rpc.getTransaction(signature(txid), {commitment: "confirmed",encoding: "base64",maxSupportedTransactionVersion: 0}).send();if (!rpcTx) throw new Error(`Transaction ${txid} not found`);const { compiledMessage, loadedAddresses } =decodeTransactionFromRpcResponse(rpcTx);
The remaining snippets reuse rpcTx, compiledMessage, and loadedAddresses
from this step.
Preparing for v1 transactions
Once the v1
format
activates, pass maxSupportedTransactionVersion: 1 or the fetch fails on v1
transactions. decodeTransactionFromRpcResponse handles legacy, 0, and
1 alike (from @solana/kit 7.0). Note that a v1 message carries its
resource limits in a transactionConfig object rather than in ComputeBudget
instructions, so code that recovers compute limits or priority fees by
scanning the decoded instructions finds nothing for v1.
Fetch with base64, base58, or json encoding.
decodeTransactionFromRpcResponse rejects jsonParsed responses - that
encoding asks the RPC node to parse instructions server-side, so there is
nothing left to decode on the client.
Walk the instructions
walkInstructions returns every instruction - outer and inner - as an array in
the order a block explorer shows: each outer instruction followed immediately by
the inner instructions its CPIs produced. Each instruction carries a trace
field describing where it sits in the call hierarchy.
import { walkInstructions } from "@solana/transaction-introspection";for (const ix of walkInstructions({compiledMessage,loadedAddresses,meta: rpcTx.meta})) {const location =ix.trace.kind === "outer"? `outer[${ix.trace.index}]`: `inner[${ix.trace.outerIndex}/${ix.trace.innerIndex}]`;console.log(location, ix.programAddress, ix.accounts?.length ?? 0);}
The trace is a discriminated union: { kind: "outer", index } for a top-level
instruction, or { kind: "inner", outerIndex, innerIndex, stackHeight? } for a
CPI instruction nested under an outer one.
meta is optional - pass rpcTx.meta directly even when it may be null. With
no meta, walkInstructions returns only the outer instructions.
Parse with a program client
Each instruction from walkInstructions is a ResolvedInstruction, so it works
directly with the predicates from @solana/instructions and the
identifyXInstruction / parseXInstruction helpers generated by the
@solana-program/* clients - no conversion step.
This example audits every Token Program SyncNative instruction in a
transaction, whether it ran at the top level or inside a CPI.
import {isInstructionForProgram,isInstructionWithAccounts,isInstructionWithData} from "@solana/instructions";import { walkInstructions } from "@solana/transaction-introspection";import {identifyTokenInstruction,parseSyncNativeInstruction,TOKEN_PROGRAM_ADDRESS,TokenInstruction} from "@solana-program/token";for (const ix of walkInstructions({compiledMessage,loadedAddresses,meta: rpcTx.meta})) {if (!isInstructionForProgram(ix, TOKEN_PROGRAM_ADDRESS)) continue;if (!isInstructionWithData(ix) || !isInstructionWithAccounts(ix)) continue;if (identifyTokenInstruction(ix) !== TokenInstruction.SyncNative) continue;const parsed = parseSyncNativeInstruction(ix);console.log(ix.trace, parsed);}
Lower-level helpers
walkInstructions covers most needs, but the package also exposes the pieces it
builds on:
getInstructionsFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)returns only the outer instructions, with resolved accounts and decoded data.getInnerInstructionsFromMeta(meta, accountMetas)decodes the inner CPI instructions from the transaction metadata.getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)returns the full account list in transaction order - the input the previous helper needs to resolve inner-instruction account indices.
import {getAccountMetasFromCompiledTransactionMessage,getInnerInstructionsFromMeta,getInstructionsFromCompiledTransactionMessage} from "@solana/transaction-introspection";const outer = getInstructionsFromCompiledTransactionMessage(compiledMessage,loadedAddresses);const accountMetas = getAccountMetasFromCompiledTransactionMessage(compiledMessage,loadedAddresses);if (!rpcTx.meta) throw new Error("Transaction metadata missing");const inner = getInnerInstructionsFromMeta(rpcTx.meta, accountMetas);
Using Rust?
There is no Rust equivalent of this package. To get parsed instructions from
Rust, request UiTransactionEncoding::JsonParsed from
getTransaction and let the RPC node resolve
accounts and inner instructions for you.
See also
- Transaction Structure - the anatomy of the response this package decodes
- Versioned Transactions - how Address Lookup Tables split accounts into static and loaded groups
getTransaction- the RPC method that returns the transaction
Is this page helpful?