in-solana by Supanode

Decoding Solana Transactions

TL;DR

  • - Use getTransaction with maxSupportedTransactionVersion: 0 to fetch raw transaction data
  • - Instructions contain program ID, account indices, and base58/base64-encoded data
  • - Anchor programs use an 8-byte discriminator; native programs use a 4-byte instruction tag

Solana transactions contain instructions, each with a program ID, account references, and encoded data. Decoding this data — turning raw bytes into meaningful operations — is essential for building explorers, indexers, analytics tools, and debugging complex transactions.

Transaction structure

A Solana transaction response from getTransaction contains:

{
  "slot": 123456789,
  "transaction": {
    "message": {
      "accountKeys": ["pubkey1", "pubkey2", "..."],
      "instructions": [
        {
          "programIdIndex": 2,
          "accounts": [0, 1],
          "data": "3Bxs3zrFhZDp"
        }
      ]
    },
    "signatures": ["sig1"]
  },
  "meta": {
    "innerInstructions": [...],
    "logMessages": [...],
    "preBalances": [...],
    "postBalances": [...]
  }
}

Fetching a transaction

const tx = await connection.getTransaction(signature, {
  maxSupportedTransactionVersion: 0,
  commitment: "confirmed",
});

Always pass maxSupportedTransactionVersion: 0 to handle both legacy and versioned transactions.

Decoding instruction data

Step 1: Identify the program

The programIdIndex points into the accountKeys array. Common programs:

ProgramID
System Program11111111111111111111111111111111
Token ProgramTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Token-2022TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Associated TokenATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
Compute BudgetComputeBudget111111111111111111111111111111

Step 2: Decode the data bytes

The data field is base58-encoded (default) or base64-encoded (with encoding: "base64") instruction data. Decode it to a byte buffer:

import bs58 from "bs58";

const dataBytes = bs58.decode(instruction.data);

Step 3: Parse based on program type

Native programs use a 4-byte little-endian instruction tag:

const instructionTag = dataBytes.readUInt32LE(0);
// System Program: 2 = Transfer, 0 = CreateAccount, ...

Anchor programs use an 8-byte discriminator (first 8 bytes of SHA256 of global:<method_name>):

const discriminator = dataBytes.slice(0, 8);
// Match against known discriminators from the IDL

Decoding common programs

System Program — Transfer

if (programId === "11111111111111111111111111111111") {
  const tag = dataBytes.readUInt32LE(0);
  if (tag === 2) { // Transfer
    const lamports = dataBytes.readBigUInt64LE(4);
    const from = accountKeys[instruction.accounts[0]];
    const to = accountKeys[instruction.accounts[1]];
    console.log(`Transfer ${lamports} lamports from ${from} to ${to}`);
  }
}

Token Program — Transfer

if (programId === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") {
  const tag = dataBytes[0];
  if (tag === 3) { // Transfer
    const amount = dataBytes.readBigUInt64LE(1);
    const source = accountKeys[instruction.accounts[0]];
    const destination = accountKeys[instruction.accounts[1]];
    const authority = accountKeys[instruction.accounts[2]];
  }
}

Anchor programs — using the IDL

import { BorshCoder } from "@coral-xyz/anchor";
import idl from "./my_program.json";

const coder = new BorshCoder(idl);
const decoded = coder.instruction.decode(dataBytes);
console.log(decoded.name, decoded.data);

The IDL contains the instruction layout, so the Borsh coder can automatically deserialize the data.

Inner instructions

Inner instructions are CPI (Cross-Program Invocation) calls made by programs during execution. They appear in meta.innerInstructions:

{
  "innerInstructions": [
    {
      "index": 0,
      "instructions": [
        {
          "programIdIndex": 3,
          "accounts": [0, 1],
          "data": "..."
        }
      ]
    }
  ]
}

The index refers to the outer instruction that triggered the CPI. A DEX swap instruction typically generates inner instructions for token transfers.

Using log messages

Transaction logs provide human-readable traces:

Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]
Program log: Instruction: Transfer
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 200000 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success

Anchor programs emit structured logs that can be parsed:

Program log: Instruction: Swap
Program data: <base64-encoded event data>

Use meta.logMessages for debugging and Program data: entries for decoding Anchor events.

Balance changes

The meta field includes pre- and post-balances:

const balanceChanges = tx.meta.postBalances.map((post, i) => ({
  account: tx.transaction.message.accountKeys[i],
  change: post - tx.meta.preBalances[i],
}));

For token balance changes, use meta.preTokenBalances and meta.postTokenBalances.

FAQ

How do I decode a transaction from a block explorer? Block explorers typically decode instructions for you. To decode programmatically, fetch the raw transaction with getTransaction and apply the parsing logic described above.

What is the difference between data and Program data: in logs? The data field in an instruction is the input to the program. Program data: log entries are outputs (events) emitted by the program during execution.

How do I handle unknown programs? Without the program’s IDL or documentation, you can only analyze the raw bytes. Look for the program’s repository or documentation to find the instruction layout. Many programs publish their IDL on-chain or in their GitHub repository.

Can I decode historical transactions? Yes, if you have access to an archive node or indexing service. The transaction format has not changed — the same decoding logic works for old transactions.

Updated: