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 2

  • Extension: 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:

  1. Basic v0: Create and send a versioned transaction

  2. 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

Last updated