Transaction Introspection

Decode a confirmed transaction and parse its instructions

Introduction

When you fetch a confirmed transaction with getTransaction, the RPC hands you a compiled, wire-shaped payload: account references are numeric indices, instruction data is encoded as a string, and the instructions emitted by cross-program invocations live in a separate meta.innerInstructions field. Codama auto-generated (e.g.@solana-program/*) clients — identifyXyzProgramInstruction, parseMyProgramInstruction, and other helpers — want Kit Instruction objects with resolved AccountMetas and raw bytes.

The @solana/transaction-introspection package bridges that gap. It decodes a getTransaction response into a CompiledTransactionMessage, resolves every account index against the static keys plus the addresses loaded from address lookup tables, normalizes the inner CPI instructions, and returns instructions in the exact form the @solana-program/* clients can identify and parse directly. It supports legacy, v0, and v1 transactions.

This guide picks up where Deserializing transactions leaves off.

Installation

These utilities are included within the @solana/kit library but you may also install them using their standalone package.

npm install @solana/transaction-introspection

Decoding an RPC response

decodeTransactionFromRpcResponse turns a getTransaction response into a DecodedRpcTransaction: the compiledMessage (always carrying the recent blockhash in lifetimeToken), the loadedAddresses pulled from meta, and — for base64 and base58 encodings only — a re-encodable transaction.

import {  } from '@solana/kit';
 
const  = await 
    .((), {
        : 'confirmed',
        : 'base64',
        : 0,
    })
    .();
if (!) {
    throw new (`Transaction ${} not found`);
}
 
const { , ,  } = ();

Prefer encoding: 'base64' when bandwidth allows — it is the most compact, the wire bytes round-trip cleanly through the Kit codecs, and the return type statically guarantees a non-optional transaction. encoding: 'base58' behaves the same way, and encoding: 'json' is accepted too, though it omits transaction because the server has already decompiled the wire format.

encoding: 'jsonParsed' is not supported. Its instructions arrive pre-parsed by the server and lack the raw bytes the @solana-program/* parseXInstruction clients need, so passing one throws SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION.

To receive a v0 or v1 transaction at all, you must set maxSupportedTransactionVersion on the getTransaction call. Without it the server rejects anything past legacy.

Walking every instruction

walkInstructions is the main API. It returns every instruction in a confirmed transaction as an array of TracedInstructions, in the order an explorer displays them: each outer instruction followed immediately by the inner instructions its CPIs produced. Because each entry is itself a resolved Kit Instruction, you can pass it straight to isInstructionForProgram and to the auto-generated identifyXInstruction / parseXInstruction helpers.

Here we tally every USDC transfer in a transaction — outer instructions and inner CPI alike — using the @solana-program/token client to parse each TransferChecked and filter by mint:

import {
    ,
    ,
    ,
    ,
    ,
    ,
} from '@solana/kit';
import {
    ,
    ,
    ,
    ,
} from '@solana-program/token';
 
const  = ('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
 
const  = await 
    .((), {
        : 'confirmed',
        : 'base64',
        : 0,
    })
    .();
if (!) {
    throw new (`Transaction ${} not found`);
}
 
const { ,  } = ();
 
let  = 0n;
for (const  of ({ , , : . })) {
    // Narrow to the Token program, then to instructions that carry data and accounts.
    if (!(, )) continue;
    if (!() || !()) continue;
    if (() !== .) continue;
 
    const { ,  } = ();
    if (.. !== ) continue;
 
     += .;
    .(
        .. === 'outer'
            ? `Transfer Checked at outer[${..}]`
            : `CPI transfer checked at inner[${..}/${..}]`,
    );
}
 
.(`Total transferred (base units): ${}`);

The CPI transfers — those a router or DEX program made on your behalf — are caught alongside the top-level ones precisely because walkInstructions interleaves the inner instructions from meta.innerInstructions.

Each entry carries a trace property typed as InstructionTrace, a discriminated union that records where the instruction sits:

  • { kind: 'outer', index } — a top-level instruction in the compiled message.
  • { kind: 'inner', outerIndex, innerIndex, stackHeight? } — an instruction emitted via cross-program invocation. stackHeight is the CPI depth, included only when the RPC reports it.

The same pattern works with any codama-generated @solana-program/* client. Swap in @solana-program/system to tally lamports moved by every TransferSol, outer or inner:

import {
    ,
    ,
    ,
    ,
} from '@solana/kit';
import {
    ,
    ,
    ,
    ,
} from '@solana-program/system';
 
let  = 0n;
for (const  of ({ , ,  })) {
    if (!(, )) continue;
    if (!() || !()) continue;
    if (() !== .) continue;
     += ()..;
}

Pass meta?.loadedAddresses (or the loadedAddresses returned by decodeTransactionFromRpcResponse) for v0 transactions that load accounts from address lookup tables. This package never makes an RPC call to resolve lookup tables itself — it uses the addresses the validator already recorded in meta. Without them, only the static accounts are available to resolve indices, and any instruction referencing a looked-up account index throws SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE. Omitting meta entirely returns only the outer instructions.

Resolving instructions without walking

If you do not need the interleaved outer-and-inner ordering, the lower-level helpers expose each piece on its own.

getInstructionsFromCompiledTransactionMessage returns just the outer instructions as ResolvedInstructions. getInnerInstructionsFromMeta returns just the inner instructions (decoding their base58 data and resolving indices against a supplied AccountMeta list). getAccountMetasFromCompiledTransactionMessage builds the ordered AccountMeta list both rely on.

import {
    ,
    ,
    ,
} from '@solana/kit';
 
const  = (
    ,
    ,
);
 
const  = (
    ,
    ,
);
const  = (, );

The account metas are returned in the runtime's resolution order — writable signers, readonly signers, writable non-signers, readonly non-signers, then the ALT-loaded writable and readonly addresses — so inner-instruction indices line up against the very same list. If you only need the flat ordered address list, map over the result with accountMetas.map((m) => m.address).

Notes

  • Version support. legacy, v0, and v1 compiled messages all resolve identically — account indices, inner instructions, and ALT-loaded addresses behave the same across versions.
  • jsonParsed is rejected. Use base64, base58, or json. Any unrecognized response shape throws SOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE.
  • TracedInstruction is a ResolvedInstruction. No separate filter helper is needed — narrow with isInstructionWithAccounts / isInstructionWithData and hand the result straight to the @solana-program/* clients.

On this page