npm.io
7.0.0 • Published 3 weeks ago

@solana/transaction-introspection

Licence
MIT
Version
7.0.0
Deps
8
Size
388 kB
Vulns
0
Weekly
0
Stars
694

npm npm-downloads
code-style-prettier

@solana/transaction-introspection

This package contains helpers for inspecting a confirmed Solana transaction's instructions — both top-level and inner CPI — in a form that the auto-generated @solana-program/* clients can identify and parse directly. It can be used standalone, but it is also exported as part of Kit @solana/kit.

The kit codecs decode a getTransaction response down to a CompiledTransactionMessage. The per-program clients (identifyTokenInstruction, parseSyncNativeInstruction, etc.) accept kit Instruction objects. This package fills the gap between them: it decodes the wire transaction, resolves account indices against static keys plus ALT-loaded addresses, normalizes the JSON-shape inner instructions from meta.innerInstructions, and returns a single list of traced instructions directly usable with the auto-generated @solana-program/* clients and with isInstructionForProgram from @solana/instructions. Supports legacy, v0, and v1 compiled transaction messages.

Quick start

Audit every Token-Program SyncNative instruction — outer or inner CPI — in a confirmed transaction:

import { createSolanaRpc, signature } from '@solana/kit';
import { isInstructionForProgram, isInstructionWithData } from '@solana/instructions';
import { decodeTransactionFromRpcResponse, walkInstructions } from '@solana/transaction-introspection';
import { identifyTokenInstruction, TOKEN_PROGRAM_ADDRESS, TokenInstruction } from '@solana-program/token';

const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
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);

for (const ix of walkInstructions({ compiledMessage, loadedAddresses, meta: rpcTx.meta })) {
    if (!isInstructionForProgram(ix, TOKEN_PROGRAM_ADDRESS) || !isInstructionWithData(ix)) continue;
    if (identifyTokenInstruction(ix) !== TokenInstruction.SyncNative) continue;
    console.log(
        `SyncNative at ${ix.trace.kind === 'outer' ? `outer[${ix.trace.index}]` : `inner[${ix.trace.outerIndex}/${ix.trace.innerIndex}]`}`,
    );
}

Functions

decodeTransactionFromRpcResponse(rpcTx)

Decodes a getTransaction response — encoding: 'base64', 'base58', or 'json' — into a DecodedRpcTransaction: the CompiledTransactionMessage (always carrying the recent blockhash in lifetimeToken), the loaded ALT addresses pulled from meta (or empty arrays for legacy transactions where meta.loadedAddresses is not present), and — for 'base64' and 'base58' only — the re-encodable wire-format Transaction.

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-undefined transaction. encoding: 'json' is also accepted, but transaction is omitted because the server has already decompiled the wire format and there are no message bytes to carry. encoding: 'jsonParsed' is not supported — its instructions arrive pre-parsed and lack raw bytes, so they cannot be round-tripped through the auto-generated parseXInstruction clients.

import { createSolanaRpc, signature } from '@solana/kit';
import { decodeTransactionFromRpcResponse } from '@solana/transaction-introspection';

const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcTx = await rpc
    .getTransaction(signature(txid), {
        commitment: 'confirmed',
        encoding: 'base64',
        maxSupportedTransactionVersion: 0,
    })
    .send();
if (!rpcTx) throw new Error('not found');

const { compiledMessage, loadedAddresses, transaction } = decodeTransactionFromRpcResponse(rpcTx);
getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)

Builds the full ordered list of AccountMetas for the message. If you only need the flat ordered Address[], map over the result: accountMetas.map(m => m.address). Roles are derived from the message header: writable signers, readonly signers, writable non-signers, readonly non-signers — followed by ALT-loaded writable (non-signer, writable) and ALT-loaded readonly (non-signer, readonly). Inner-instruction account indices reference the same flat list, so the result is also useful for resolving inner instructions.

import { getAccountMetasFromCompiledTransactionMessage } from '@solana/transaction-introspection';

const accountMetas = getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses);
getInstructionsFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)

Returns the outer instructions of a compiled transaction message as ResolvedInstruction[]. Each instruction has its account indices resolved to AccountMetas (with proper signer/writable bits) and its data exposed as a ReadonlyUint8Array — the form the auto-generated @solana-program/* parseXInstruction and identifyXInstruction functions expect. Following the kit Instruction conventions, accounts and data are present only when non-empty, so isInstructionWithAccounts and isInstructionWithData behave as expected.

import { isInstructionWithData } from '@solana/instructions';
import { getInstructionsFromCompiledTransactionMessage } from '@solana/transaction-introspection';
import { identifyTokenInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';

const instructions = getInstructionsFromCompiledTransactionMessage(compiledMessage, loadedAddresses);
for (const ix of instructions) {
    if (ix.programAddress === TOKEN_PROGRAM_ADDRESS && isInstructionWithData(ix)) {
        const kind = identifyTokenInstruction(ix);
        // ...
    }
}
getInnerInstructionsFromMeta(meta, accountMetas)

Returns the inner instructions in a getTransaction response as TracedInstructions. The RPC returns inner instructions in a different shape from the wire format — indices reference the same flat account list as outer instructions, but data is base58-encoded. This helper decodes the data, resolves the indices against the supplied AccountMeta list, and tags each instruction with an inner trace (carrying outerIndex, innerIndex, and stackHeight when the RPC provides one).

import {
    getAccountMetasFromCompiledTransactionMessage,
    getInnerInstructionsFromMeta,
} from '@solana/transaction-introspection';

const accountMetas = getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses);
const inner = getInnerInstructionsFromMeta(rpcTx.meta, accountMetas);
walkInstructions({ compiledMessage, meta?, loadedAddresses? })

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. Each entry is itself a ResolvedInstruction (with addresses and roles already resolved) carrying a trace property that records whether the instruction is outer or inner (with stack height when the RPC provides it).

Because each entry is a ResolvedInstruction, you can pass it directly to isInstructionForProgram from @solana/instructions (which narrows the programAddress type) and to the auto-generated identifyXInstruction / parseXInstruction helpers — no separate filter helper is needed.

If meta is omitted, only outer instructions are returned. If loadedAddresses is omitted, only static accounts are used to resolve indices — pass meta?.loadedAddresses for v0 transactions that load accounts from address lookup tables.

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;
    // `ix.programAddress` is narrowed to TOKEN_PROGRAM_ADDRESS.
    if (!isInstructionWithData(ix) || !isInstructionWithAccounts(ix)) continue;
    if (identifyTokenInstruction(ix) === TokenInstruction.SyncNative) {
        const parsed = parseSyncNativeInstruction(ix);
        console.log(ix.trace, parsed);
    }
}

The same pattern works with any auto-generated @solana-program/* client. Here, tally the lamports moved by every System-program TransferSol — outer or inner CPI:

import { isInstructionForProgram } from '@solana/instructions';
import { walkInstructions } from '@solana/transaction-introspection';
import {
    identifySystemInstruction,
    parseTransferSolInstruction,
    SYSTEM_PROGRAM_ADDRESS,
    SystemInstruction,
} from '@solana-program/system';

let totalLamports = 0n;
for (const ix of walkInstructions({ compiledMessage, loadedAddresses, meta: rpcTx.meta })) {
    if (!isInstructionForProgram(ix, SYSTEM_PROGRAM_ADDRESS)) continue;
    if (identifySystemInstruction(ix) !== SystemInstruction.TransferSol) continue;
    totalLamports += parseTransferSolInstruction(ix).data.amount;
}

Types

LoadedAddresses

The shape of meta.loadedAddresses from getTransaction. Two arrays — writable and readonly — kept in the same order the runtime uses to resolve instruction account indices.

import type { LoadedAddresses } from '@solana/transaction-introspection';

const loaded: LoadedAddresses = rpcTx.meta?.loadedAddresses ?? { readonly: [], writable: [] };
DecodedRpcTransaction

{ compiledMessage, loadedAddresses, transaction? }. compiledMessage always carries a lifetimeToken (the recent blockhash). transaction is present only for 'base64' and 'base58' responses; the dispatcher's overloads narrow it to a non-optional Transaction for those encodings.

The input side of decodeTransactionFromRpcResponse is typed with the GetTransactionApiResponseBase64, GetTransactionApiResponseBase58, and GetTransactionApiResponseJson types from @solana/rpc-api — the non-null response shapes of the corresponding getTransaction encodings.

ResolvedInstruction<TProgramAddress>

An Instruction whose account indices have been resolved to AccountMetas and whose data is exposed as a ReadonlyUint8Array. Directly usable with the auto-generated @solana-program/* parseXInstruction and identifyXInstruction functions, and with isInstructionForProgram from @solana/instructions (which narrows the TProgramAddress parameter). accounts and data are present only when non-empty — use isInstructionWithAccounts / isInstructionWithData to narrow.

InstructionTrace

A discriminated union recording an instruction's location within a transaction:

  • { 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 included only when reported by the RPC.
TracedInstruction<TProgramAddress>

A ResolvedInstruction<TProgramAddress> with an extra trace: InstructionTrace property — one entry returned by walkInstructions. Because it is itself a ResolvedInstruction, it can be passed directly to isInstructionForProgram and to the auto-generated identifyXInstruction / parseXInstruction helpers.

MetaWithInnerInstructions

A structural type capturing the minimum shape of getTransaction's meta field that getInnerInstructionsFromMeta needs. Accepting a structural type keeps callers free to pass the full RPC response without coupling to a specific overload.

Notes

  • Supports legacy, v0, and v1 compiled transaction messages. Pass any version through decodeTransactionFromRpcResponse and walkInstructions — account indices, inner instructions, and ALT-loaded addresses resolve identically across versions. To actually receive v0 or v1 from the RPC you still need to set maxSupportedTransactionVersion on the getTransaction call — without it, the server downgrades anything past legacy to an error.
  • decodeTransactionFromRpcResponse accepts encoding: 'base64', 'base58', or 'json'. 'jsonParsed' is not supported — its instructions arrive pre-parsed by the server and lack raw bytes.

Keywords