Solana transaction v1 migration: readers, wallets and indexers

In Solana transaction v1 explained, we decoded a confirmed v1 transfer byte by byte. Now we look at the code around those bytes. Reading a v1 transaction is where existing integrations break first, because blocks on mainnet now mix legacy, v0 and v1 transactions.
We will read the same devnet transfer with the wrong and the right RPC settings, decode it with Kit, and then rebuild a v0 transfer as v1 and update an indexer. By the end, you will have a checklist for each side of an integration.
Short answer: To migrate to Solana transaction v1, readers must pass
maxSupportedTransactionVersion: 1ongetTransactionandgetBlock, wallets must rebuild transactions as v1 messages with inline addresses and a total priority fee in lamports, and indexers must branch on the transaction version when they resolve accounts and read budgets. See also Chainstack’s Solana transaction v1 guide.
Update readers as well as senders
A service can encounter v1 transactions without ever constructing one. We’ll read the confirmed devnet transfer from part one again, changing only the transaction versions the client declares it can handle. The setup is two lines:
import { createSolanaRpc, devnet, signature } from '@solana/kit';
const rpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
const tx = signature(
'7yYRUqcBbHTPkop1xJu7UDUPJUagXaktxhWLm5sjGJn2QHAnoZsXd5gpvEJtLndLdF9ECFzkEees3KAAXJMNjzA',
);
With those in place, this request declares support up to version 0:
await rpc.getTransaction(tx, {
encoding: 'json',
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
}).send();
The RPC rejects it with error code -32015 because the transaction is version 1. The corrected request is:
const receipt = await rpc.getTransaction(tx, {
encoding: 'json',
commitment: 'confirmed',
maxSupportedTransactionVersion: 1,
}).send();
Setting this value to 1 declares support for legacy, v0, and v1 transactions. It does not filter the response to a particular version. Transaction reader migration
The same requirement applies when reading the block containing our transfer. A blockSubscribe subscription behaves differently: at a lower version it stays open but delivers block: null instead of an error, as described in Chainstack’s v1 reader guide.
await rpc.getBlock(511_271_356n, {
encoding: 'json',
commitment: 'confirmed',
transactionDetails: 'full',
rewards: false,
maxSupportedTransactionVersion: 1,
}).send();
We tested both settings against the confirmed devnet transaction and its block:
getTransaction
maxSupportedTransactionVersion: 0→ Error-32015.
maxSupportedTransactionVersion: 1→ Returns the v1 transaction.
getBlock with full transactions
maxSupportedTransactionVersion: 0→ Error-32015, the entire request fails.
maxSupportedTransactionVersion: 1→ Returns the block, including our transfer.

On Chainstack nodes. The behavior is the same on a Chainstack Solana endpoint, and the v1 reader guide lists it per method.
maxSupportedTransactionVersionis a ceiling, not a filter, so declaring1returns legacy, v0 and v1 transactions alike. It must be the JSON integer1; the string"1"fails validation with-32602.
| Method | With maxSupportedTransactionVersion: 1 | At a lower value or omitted |
|---|---|---|
getBlock | Returns the block | The whole call fails with -32015 |
getTransaction | Returns the transaction | Fails with -32015 on a v1 transaction |
blockSubscribe | Streams blocks | Stays open, but each notification carries block: null |
Calls that return no transaction bodies are unaffected: transactionDetails set to signatures or none, and getSignaturesForAddress, work at any version.
An unsupported transaction causes the full block request to fail. The RPC does not quietly remove it and return the remaining transactions. getBlock configuration
Raising the supported version is only one part of the update. A reader requesting base64 must also understand the returned bytes. Kit’s decoding path separates the transaction envelope from its message:
const wire = Buffer.from(base64Transaction, 'base64');
const transaction = getTransactionDecoder().decode(wire);
const compiled = getCompiledTransactionMessageDecoder()
.decode(transaction.messageBytes);
const message = decompileTransactionMessage(compiled);
if (message.version === 1) {
console.log(message.config);
}
For v1, the message starts at byte zero and signatures appear at the end. A reader that always skips a leading signature count and signatures would extract the wrong message bytes. Kit transaction decoder
Reading v1 transactions through a JavaScript SDK also needs a recent library. Chainstack’s guide lists @solana/web3.js 1.99.0 or later, or @solana/kit 7.1.0 or later. Earlier @solana/web3.js releases throw a validation error on any getBlock, getTransaction or getParsedTransaction response that contains a v1 transaction, even with maxSupportedTransactionVersion: 1.
There is also a naming difference between Kit objects and JSON-RPC responses. Kit exposes the priority fee as message.config.priorityFeeLamports. Our devnet transaction’s JSON response instead contains this object inside transaction.message:
{
"transactionConfig": {
"computeUnitLimit": 20000,
"heapSize": 65536,
"loadedAccountsDataSizeLimit": 65536,
"priorityFee": 5000
}
}
The JSON path is transaction.message.transactionConfig.priorityFee, and the value is the total priority fee in lamports. Looking for transaction.message.config would miss the configuration.
Migrate v0 integrations to v1
A wallet integration needs to construct, review, and sign the new format. An indexer needs to interpret v1 alongside existing legacy and v0 transactions.
Migration for wallets
Let’s take an existing v0 transfer and rebuild it as v1. We keep the transfer amount, sender, recipient, and account permissions. We change how the transaction carries its addresses and resource settings.
The migration has three main changes:
- Addresses: Resolve any ALT references and include the full addresses inline.
- Resource limits: Move the settings from ComputeBudget instructions into the message configuration.
- Priority fee: Convert the old micro-lamports-per-CU price into a total fee in lamports.

Using the sender, transfer and fresh lifetime from the transfer script in part one, the rebuilt message is:
const v1Message = pipe(
createTransactionMessage({ version: 1 }),
m => setTransactionMessageFeePayerSigner(sender, m),
m => setTransactionMessageLifetimeUsingBlockhash(lifetime, m),
m => appendTransactionMessageInstructions([transfer], m),
m => setTransactionMessageConfig({
computeUnitLimit: 20_000,
loadedAccountsDataSizeLimit: 64 * 1024,
heapSize: 64 * 1024,
priorityFeeLamports: 5_000n,
}, m),
);
The instruction list now contains the System Program transfer. Its resource settings are attached to the message through setTransactionMessageConfig.
If our previous v0 fee request was 250,000 micro-lamports per CU with a 20,000-CU limit:
20,000 × 250,000 ÷ 1,000,000 = 5,000 lamports
V1 expresses that total directly as priorityFeeLamports: 5_000n.
The transfer instruction already contains the sender and recipient addresses. For a v0 transaction reconstructed from serialized bytes, resolve its lookup-table accounts before building this message. The v1 compiler then places those addresses inline.
Submitting through Chainstack. A rebuilt v1 transaction can exceed 1,232 bytes. Submit it as
base64:sendTransactionrejects a v1 transaction over that size sent asbase58with-32602and atoo largemessage. The fee a v1 transaction pays is exactly5000 * numRequiredSignatures + priorityFee, which is a quick way to check your conversion.
Finally, simulate the rebuilt transaction, finalize its resource limits, and have every required signer approve and sign the new message. The existing v0 signatures cover different bytes and cannot be reused.
Migration for indexers
An indexer preserves transactions in their original versions. Its ingestion pipeline must handle legacy, v0, and v1 together.
As shown in the previous section, request transaction data with:
maxSupportedTransactionVersion: 1
Apply this to both getTransaction and getBlock requests that return full transactions. Setting it to 0 makes a block request fail when the block contains v1; it does not filter out the v1 entries.
After fetching a record, resolve account indexes according to its version. For an unparsed encoding: 'json' response:
- Legacy: Use
transaction.message.accountKeys.
- V0: Use static
accountKeys, followed bymeta.loadedAddresses.writable, thenmeta.loadedAddresses.readonly.
- V1: Use
transaction.message.accountKeys, all addresses are inline.
Program indexes, instruction account indexes, and balance indexes refer to this account ordering. Using only v0’s static keys can leave lookup-table accounts unresolved or attribute activity incorrectly. If required lookup metadata is missing, retain the record for recovery instead of guessing. jsonParsed has a different representation and needs its own handling. RPC response structures
Budget extraction also needs a version branch:
- Legacy and v0: Interpret ComputeBudget instructions and the defaults that apply when settings are absent.
- V1: Read
transaction.message.transactionConfig.

Keep the requested settings separate from execution results:
transactionConfig.computeUnitLimit: Requested compute limit.
transactionConfig.priorityFee: Requested total priority fee in lamports.
meta.computeUnitsConsumed: Compute actually consumed.
meta.fee: Total fee charged, including the base fee.
Our confirmed transfer requested 20,000 CU, consumed 150 CU, requested a 5,000-lamport priority fee, and paid a 10,000-lamport total fee. Those four values describe different things.
For raw base64 input, use the version-aware decoder from the previous section to locate the message and signatures. Keep the original bytes and transaction version so records can be decoded again later.
Backfills and streams on Chainstack. A full Solana node keeps roughly the last day and a half of history, so a backfill that reads older blocks needs archive access, and each of those
getBlockcalls still needsmaxSupportedTransactionVersion: 1. For live data,blockSubscribeat a lower version keeps streaming but deliversblock: null, which is why Chainstack’s docs point production streaming to Yellowstone gRPC Geyser, a paid add-on on mainnet.
Yellowstone gRPC consumers need updated producer and client support as well. Its schema identifies v1 through the presence of Message.config. Check that before the versioned boolean, which is true for both v0 and v1:
const version =
message.config != null ? 1 :
message.versioned ? 0 :
'legacy';
An empty configuration object still counts as present. A stale generated client may discard this field and make v1 appear to be v0. Yellowstone Message schema
Migration checklist
Use this list for both sides of an integration. It follows the readiness checklist in Chainstack’s v1 guide.
- Raise
maxSupportedTransactionVersionto1on everygetBlock,getTransactionandblockSubscribecall, as the integer1. - Upgrade the JavaScript SDK to
@solana/kit7.1.0 or later, or@solana/web3.js1.99.0 or later. - Read limits from
transactionConfigfirst, and fall back to the ComputeBudget instruction scan for legacy and v0. - Treat
transactionConfig.priorityFeeas lamports, and convert v0 prices withprice * computeUnitLimit / 1,000,000before comparing. - Handle versions 0, 1 and legacy in one pipeline, and branch on the
versionfield instead of assuming a shape. - Submit anything over 1,232 raw bytes as
base64. - Drop lookup tables from v1 builds, and place accounts inline within the 64-address limit.
Summary
Migrating to Solana transaction v1 is mostly about reading and rebuilding correctly. Readers must declare version 1, wallets must rebuild and re-sign the message with inline addresses and a lamport priority fee, and indexers must resolve accounts and budgets per version while keeping the original bytes. Legacy and v0 keep working, so every pipeline has to handle all three formats at once.
For the byte layout behind these changes, see part one.
Frequently asked questions
Yes, if you read transactions or blocks. Pass maxSupportedTransactionVersion: 1 as the integer 1 on getTransaction and getBlock. Without it, getTransaction fails with error -32015 on a v1 transaction, and getBlock fails entirely if the block contains one.
Chainstack’s guide lists @solana/kit 7.1.0 or later and @solana/web3.js 1.99.0 or later. Earlier @solana/web3.js releases throw a validation error on responses that contain a v1 transaction, and earlier @solana/kit releases return version as the bigint 1n instead of the number 1.
The subscription stays open and keeps advancing, but each notification carries block: null and an UnsupportedTransactionVersion error instead of block data. Set maxSupportedTransactionVersion: 1 in the subscription options.
Multiply the compute unit price in micro-lamports by the compute unit limit, divide by 1,000,000 and round up to whole lamports. For example, 250,000 micro-lamports per CU with a 20,000-CU limit gives a 5,000-lamport priority fee, which v1 sets directly as priorityFeeLamports.
Yes, for v0 transactions. A v1 transaction has every address inline, so its account keys are in transaction.message.accountKeys. A v0 transaction needs its static keys followed by meta.loadedAddresses.writable and then meta.loadedAddresses.readonly.
Check whether Message.config is present before you look at the versioned boolean, which is true for both v0 and v1. A stale generated client can drop the config field and make v1 transactions look like v0.
Related reading
- Solana transaction v1 explained: bytes, budgets, signatures (part one)
- Solana: Transaction v1 explained (Chainstack docs)
- getTransaction and getBlock (Chainstack API reference)
- Yellowstone gRPC Geyser plugin (Chainstack docs)
- Solana archive nodes (Chainstack docs)
- Solana Address Lookup Tables: Boost Transactions