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:
| Program | ID |
|---|---|
| System Program | 11111111111111111111111111111111 |
| Token Program | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Token-2022 | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb |
| Associated Token | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| Compute Budget | ComputeBudget111111111111111111111111111111 |
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: