Sending a Versioned Transaction
Welcome to the ultimate guide for Solana's v0 transactions and Address Lookup Tables (LUTs)! This guide uses real working code from the Jelli dApp Playground that you can test right now.
๐ฏ What Are Versioned Transactions?
Solana supports two transaction types:
Legacy: The old format (max 35 accounts)
v0: The new hotness with Address Lookup Table support (max 256 accounts!)
Think of v0 transactions as the difference between sending a postcard (legacy) vs sending a package with a shipping manifest (v0 + LUTs). The manifest lets you reference way more stuff without cluttering the package itself.
๐ฎ Interactive Playground
You can test everything in this guide live:
Basic v0 Transactions:
/sending-a-versioned-transaction/
Full LUT Workflow:
/address-lookup-tables/
๐ค Part 1: Your First Versioned Transaction
Let's start simple - sending a basic v0 transaction without LUTs.
Setup Your HTML
<!DOCTYPE html>
<html>
<head>
<title>Versioned Transactions with Jelli</title>
</head>
<body>
<!-- Load Solana Web3.js via CDN -->
<script src="https://cdn.jsdelivr.net/npm/@solana/web3.js@latest/lib/index.iife.min.js"></script>
<script src="your-script.js"></script>
</body>
</html>
Create Your First v0 Transaction
// Extract classes from global object (CDN style)
const { Connection, SystemProgram, TransactionMessage, VersionedTransaction } = solanaWeb3;
async function sendVersionedTransaction() {
// Get your Jelli wallet
const provider = window.jelli.solana;
const connection = new Connection('https://api.devnet.solana.com');
// Connect wallet
const { publicKey } = await provider.connect();
const { blockhash } = await connection.getLatestBlockhash();
// Create a simple transfer (to yourself for testing)
const instructions = [
SystemProgram.transfer({
fromPubkey: publicKey,
toPubkey: publicKey, // Self-transfer
lamports: 1000000, // 0.001 SOL
}),
];
// ๐ฏ The v0 magic happens here!
const messageV0 = new TransactionMessage({
payerKey: publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message(); // Notice: compileToV0Message()
const transactionV0 = new VersionedTransaction(messageV0);
// Debug info - see what you created!
console.log('โ
V0 Transaction Details:', {
version: transactionV0.version, // Should be 0
hasLUTs: messageV0.addressTableLookups.length > 0
});
// Send it!
const { signature } = await provider.signAndSendTransaction(transactionV0);
// Verify with v0 support
await connection.getSignatureStatus(signature);
console.log('๐ Transaction confirmed:', signature);
// Get details later (requires v0 support)
const txDetails = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0 // ๐จ Critical for v0 transactions!
});
}
๐ Congratulations! You just sent a versioned transaction. But we're just getting started...
๐๏ธ Part 2: Address Lookup Tables - The Real Power
Now for the fun part - LUTs! Think of them as address books that let you pack way more accounts into a single transaction.
Step 1: Create an Address Lookup Table
const { AddressLookupTableProgram } = solanaWeb3;
async function createLUT() {
const provider = window.jelli.solana;
// ๐ฅ LUT creation needs 'confirmed' commitment for reliability
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const { publicKey } = await provider.connect();
// ๐ฏ Slot timing is critical for LUTs!
// Use confirmed slot with buffer for reliability (simple approach often fails)
const confirmedSlot = await connection.getSlot('confirmed');
const recentSlot = confirmedSlot - 2;
const { blockhash } = await connection.getLatestBlockhash();
console.log('๐ Using confirmed slot:', confirmedSlot);
console.log('๐ LUT recentSlot:', recentSlot, '(confirmed - 2)');
// Check you have enough SOL (LUTs cost rent)
const balance = await connection.getBalance(publicKey);
console.log('๐ฐ Balance:', (balance / 1000000000).toFixed(4), 'SOL');
// Create proper PublicKey objects (avoids weird errors)
const authorityPubkey = new PublicKey(publicKey.toString());
const payerPubkey = new PublicKey(publicKey.toString());
// ๐ฏ Create the LUT instruction
const [lookupTableInst, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({
authority: authorityPubkey,
payer: payerPubkey,
recentSlot: recentSlot,
});
console.log('๐๏ธ LUT Address:', lookupTableAddress.toBase58());
// Send the creation transaction
const lookupMessage = new TransactionMessage({
payerKey: payerPubkey,
recentBlockhash: blockhash,
instructions: [lookupTableInst],
}).compileToV0Message();
const lookupTransaction = new VersionedTransaction(lookupMessage);
const { signature } = await provider.signAndSendTransaction(lookupTransaction);
console.log('โ
LUT Created! Signature:', signature);
return lookupTableAddress; // Save this for the next steps!
}
Step 2: Extend Your LUT (Add Addresses)
async function extendLUT(lookupTableAddress) {
const provider = window.jelli.solana;
// Extension uses standard connection (no 'confirmed' needed)
const connection = new Connection('https://api.devnet.solana.com');
const { publicKey } = await provider.connect();
// ๐ IMPORTANT: LUTs need to "warm up" after creation
console.log('โฐ Waiting for LUT to activate...');
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second wait
// Add some useful addresses to your LUT
const addressesToAdd = [
publicKey, // Your wallet
SystemProgram.programId, // System Program
new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), // Token Program
new PublicKey('11111111111111111111111111111112'), // System Program again
];
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
payer: publicKey,
authority: publicKey,
lookupTable: lookupTableAddress,
addresses: addressesToAdd,
});
// Send the extension transaction
const extensionMessage = new TransactionMessage({
payerKey: publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: [extendInstruction],
}).compileToV0Message();
const extensionTransaction = new VersionedTransaction(extensionMessage);
const { signature } = await provider.signAndSendTransaction(extensionTransaction);
console.log('๐ LUT Extended! Added', addressesToAdd.length, 'addresses');
console.log('๐ Extension signature:', signature);
}
Step 3: Use Your LUT in a Transaction
async function useWithLUT(lookupTableAddress) {
const provider = window.jelli.solana;
const connection = new Connection('https://api.devnet.solana.com');
const { publicKey } = await provider.connect();
// Fetch your LUT from the blockchain
const lutResponse = await connection.getAddressLookupTable(lookupTableAddress);
const lookupTableAccount = lutResponse.value;
if (!lookupTableAccount) {
throw new Error('LUT not found or not activated yet. Wait a bit more!');
}
console.log('๐ LUT loaded with', lookupTableAccount.state.addresses.length, 'addresses:');
lookupTableAccount.state.addresses.forEach((addr, i) => {
console.log(` ${i}: ${addr.toBase58()}`);
});
// Create a transaction that uses the LUT
const instructions = [
SystemProgram.transfer({
fromPubkey: publicKey,
toPubkey: publicKey, // Self-transfer
lamports: 1000, // Tiny amount
}),
];
// ๐ฏ The LUT magic! Pass the LUT account to compileToV0Message
const messageV0 = new TransactionMessage({
payerKey: publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions,
}).compileToV0Message([lookupTableAccount]); // Array of LUTs goes here!
console.log('๐ V0 Message with LUT:', {
lutCount: messageV0.addressTableLookups.length,
staticAccounts: messageV0.staticAccountKeys.length
});
const transactionV0 = new VersionedTransaction(messageV0);
const { signature } = await provider.signAndSendTransaction(transactionV0);
// Verify with proper v0 support
await connection.getSignatureStatus(signature);
console.log('๐ LUT-powered transaction sent!', signature);
// Get details with v0 support
const txDetails = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0
});
console.log('๐ Transaction details:', txDetails);
}
๐ช Putting It All Together
Here's the complete flow:
async function fullLUTDemo() {
try {
console.log('๐ Starting full LUT demo...');
// Step 1: Create LUT
const lutAddress = await createLUT();
// Step 2: Extend LUT
await extendLUT(lutAddress);
// Step 3: Use LUT
await useWithLUT(lutAddress);
console.log('๐ Full LUT demo complete!');
} catch (error) {
console.error('โ Demo failed:', error.message);
}
}
// Run it!
fullLUTDemo();
๐จ Critical Gotchas
1. maxSupportedTransactionVersion
Always add this when fetching v0 transactions:
const txDetails = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0 // Without this, it fails on v0 transactions!
});
2. LUT Timing
Creation: Use
connection.getSlot('confirmed')
then subtract 2Extension: Wait 5+ seconds after creation
Usage: Wait 5+ seconds after extension
3. Connection Commitments
LUT Creation:
new Connection(url, 'confirmed')
Everything Else:
new Connection(url)
(no commitment)
๐ฏ Why Use LUTs?
Before LUTs: Max 35 accounts per transaction With LUTs: Max 256 accounts per transaction!
Perfect for:
๐ช Complex DeFi operations
๐ฎ Gaming with many assets
๐๏ธ DAO governance with many participants
๐ Batch operations
๐งช Test Your Skills
Try the interactive examples:
Basic v0: Create and send a versioned transaction
LUT Master: Build the full create โ extend โ use workflow
Each example has detailed console logging so you can see exactly what's happening!
๐ Quick Reference
// Basic v0 transaction
const messageV0 = new TransactionMessage({...}).compileToV0Message();
const tx = new VersionedTransaction(messageV0);
// v0 transaction with LUT
const messageV0 = new TransactionMessage({...}).compileToV0Message([lookupTableAccount]);
// Always use this for v0 transactions
await connection.getTransaction(signature, { maxSupportedTransactionVersion: 0 });
Happy building with Solana v0 transactions! ๐
๐ฎ Try the live examples: Visit
/sending-a-versioned-transaction/
and/address-lookup-tables/
to test everything in this guide!
๐ References
Solana Documentation: Versioned Transactions
Solana Documentation: https://solana.com/developers/guides/advanced/lookup-tables
Anvit's Deep Dive: Versioned Transactions Blog
Last updated