Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Solana Versioned Transactions and getBlock/getTransaction Parsing: v0, Address Lookup Tables, and Decoding

Learn how to parse Solana versioned transactions (v0) returned by getBlock and getTransaction, including address lookup tables, version flags, and decoding steps with a runnable script.

TL;DR

A deep dive into Solana versioned transactions (v0) and how to correctly parse them from getBlock/getTransaction RPC responses, covering address lookup tables, version flags, and a runnable decoding script.

Direct Answer: Parsing Solana Versioned Transactions from getBlock/getTransaction

When you call getBlock or getTransaction on a Solana RPC endpoint, the transaction object you receive may be a legacy transaction or a versioned transaction (v0). The key to parsing them correctly is to inspect the first byte of the transaction message: if the high bit (0x80) is set, it's a v0 transaction that uses address lookup tables (ALTs). For v0 transactions, the account list is not fully inline; part of it is referenced via lookup tables, so a naive parser that assumes all accounts are inline will produce wrong results. This article explains the mechanism and provides a reproducible script to decode both types.

The Solana documentation on Versioned Transactions and Address Lookup Tables are the primary protocol sources. The Solana RPC getBlock and getTransaction method docs also describe the response formats in detail.

How Versioned Transactions Work

Solana transactions have a wire format that is base58-encoded (or base64 when using JSON-RPC with encoding: 'base64'). The transaction message begins with a header that includes a version flag. Legacy messages start with a short-u16 number of required signatures, followed by the number of read-only signed accounts, etc. In contrast, v0 messages set the high bit of the first byte (0x80) to indicate they are versioned, and then encode the version number (currently 0) in the lower bits.

The main difference is that v0 messages can include an addressTableLookups section. This section lists one or more lookup table addresses, each with a list of indexes into that table. The actual account public keys are not stored inline in the message; they are resolved by fetching the table's account data from the blockchain. This allows transactions to reference more than the legacy 32-account static limit, because the table can hold up to 256 accounts, and each lookup adds only a few bytes to the message.

For a detailed explanation, see the Solana docs on Versioned Transactions - v0: Address Lookup Tables.

Parsing getBlock and getTransaction Responses

When you request a transaction with getTransaction or getBlock, you can specify an encoding. The default is json, which returns a parsed representation if you use getParsedTransaction or getParsedBlock. In the parsed format, the transaction object includes a message with accountKeys (an array of objects with pubkey and signer/writable flags), instructions, and addressTableLookups (if any). The version field indicates whether it's 'legacy' or '0'.

If you request encoding: 'jsonParsed', the RPC returns a fully parsed transaction with human-readable instruction data. However, if you request encoding: 'base58' or encoding: 'base64', you get the raw transaction blob, which you must decode yourself. The raw blob is what you need to parse if you want to understand the wire format or if you're building a low-level tool.

The Solana RPC docs for getTransaction and getBlock describe the response formats. For a community perspective, see the Solana versioned-transactions guide.

Step-by-Step Decoding with a Runnable Script

Below is a Node.js script that fetches a recent transaction (you supply the signature), inspects the version flag, expands address table lookups, and decodes the instructions. It uses the @solana/web3.js library, which handles the low-level parsing for you. The script prints a summary of the transaction, including the version, account keys, and instruction details.

To run it, you'll need to install the dependencies and provide your own RPC URL and a recent transaction signature. The script is self-contained and uses only public libraries.

// decode-solana-tx.js
// Usage: node decode-solana-tx.js <RPC_URL> <TX_SIGNATURE>
// Example: node decode-solana-tx.js https://api.mainnet-beta.solana.com <signature>

const web3 = require('@solana/web3.js');

async function main() {
  const rpcUrl = process.argv[2];
  const signature = process.argv[3];
  if (!rpcUrl || !signature) {
    console.error('Please provide RPC URL and transaction signature.');
    process.exit(1);
  }

  const connection = new web3.Connection(rpcUrl, 'confirmed');
  const tx = await connection.getParsedTransaction(signature, {
    maxSupportedTransactionVersion: 0, // allow v0
  });

  if (!tx) {
    console.error('Transaction not found.');
    process.exit(1);
  }

  console.log('Transaction version:', tx.version);
  console.log('Slot:', tx.slot);
  console.log('Block time:', tx.blockTime);

  const meta = tx.meta;
  if (meta) {
    console.log('Fee:', meta.fee);
    console.log('Logs:', meta.logMessages);
  }

  const message = tx.transaction.message;
  console.log('Account keys:');
  message.accountKeys.forEach((key, i) => {
    console.log(`  [${i}] ${key.pubkey.toString()} (signer: ${key.signer}, writable: ${key.writable})`);
  });

  if (message.addressTableLookups && message.addressTableLookups.length > 0) {
    console.log('Address table lookups:');
    for (const lookup of message.addressTableLookups) {
      console.log(`  Table: ${lookup.accountKey.toString()}`);
      console.log(`    Writable indexes: ${lookup.writableIndexes.join(', ')}`);
      console.log(`    Readonly indexes: ${lookup.readonlyIndexes.join(', ')}`);
    }
  }

  console.log('Instructions:');
  for (const ix of message.instructions) {
    console.log(`  Program: ${ix.programId.toString()}`);
    console.log(`    Accounts: ${ix.accounts.map(a => a.toString()).join(', ')}`);
    console.log(`    Data: ${ix.data}`);
  }
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

Expected Output and Verification

When you run the script with a valid v0 transaction signature, you'll see output similar to the following (actual values will vary):

The script prints the transaction version, slot, fee, account keys, address table lookups (if any), and decoded instructions. To verify correctness, you can cross-check the account keys and instruction data with a block explorer like Solscan or Solana Explorer.

If the transaction is legacy, the version field will be 'legacy' and there will be no addressTableLookups. The script handles both cases.

Transaction version: 0
Slot: 123456789
Block time: 1699999999
Fee: 5000
Logs: [...]
Account keys:
  [0] 11111111111111111111111111111111 (signer: true, writable: true)
  [1] ...
Address table lookups:
  Table: 8mU... (signer: false, writable: false)
    Writable indexes: 1, 2
    Readonly indexes: 0
Instructions:
  Program: 11111111111111111111111111111111
    Accounts: 11111111111111111111111111111111, ...
    Data: 3Bxs...

Common Failures and Fixes

When parsing Solana transactions, you may encounter several common issues. The table below maps symptoms to causes and fixes.

  • Transaction won't parse: If you're using a library that doesn't support v0 (e.g., an older version of @solana/web3.js), you'll get an error. Fix: upgrade to a version that supports maxSupportedTransactionVersion: 0.
  • Account list looks wrong: If you parse a v0 transaction as legacy, the account list will be incomplete or incorrect because it doesn't include the lookup table accounts. Fix: always check the version flag and expand address table lookups.
  • ALT disabled: Some RPC endpoints or libraries may disable address lookup tables by default. Fix: explicitly set maxSupportedTransactionVersion: 0 in your request.
  • base58 vs base64: If you request encoding: 'base58', you get a base58 string; if you request encoding: 'base64', you get a base64 string. Make sure your decoder matches the encoding.
  • Missing lookup table data: To expand address table lookups, you need to fetch the table's account data. If the table has been closed or is unavailable, decoding fails. Fix: ensure the table still exists on-chain.

Tradeoffs and Limitations

Versioned transactions with address lookup tables offer significant benefits: they allow more accounts per transaction, reduce transaction size, and enable more complex instructions. However, they introduce a dependency on the lookup table's existence and require an extra RPC call to fetch the table data if you're decoding manually.

The legacy format is simpler and self-contained, but limited to 32 accounts. For most modern Solana applications, v0 is the standard, and you should design your tooling to handle both.

When using a public RPC endpoint, you may encounter rate limits or latency issues. For production, consider a dedicated endpoint from a provider like OnFinality. See our RPC pricing and API service pages for options.

Next Steps and Further Reading

Now that you understand how to parse versioned transactions, you can apply this knowledge to build more robust Solana applications. For more Solana RPC topics, check out our guides on querying historical data, WebSocket subscriptions, and timeouts and retries.

If you're working with multiple RPC calls, see our JSON-RPC batching best practices. For a complete list of Solana RPC methods, refer to the Solana JSON-RPC methods (RPC Assistant).

Explore the OnFinality Learn hub for more tutorials, and don't forget to check out our Solana network page for endpoint details.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started