Summary #
- Versioned Transactions in Solana allows support for both legacy and newer transaction formats. The original format is referred to as "legacy," while new formats begin at version 0. Versioned transactions were introduced to accommodate the use of Address Lookup Tables (LUTs).
- Address Lookup Tables are special accounts that store the addresses of other accounts. In versioned transactions, these addresses can be referenced by a 1-byte index instead of the full 32-byte address. This optimization enables more complex transactions than previously possible.
Lesson #
By design, Solana transactions are limited to 1232 bytes. Transactions exceeding this limit will fail, which restricts the size of atomic operations that can be performed. While this limit allows for optimizations at the network level, it imposes restrictions on transaction complexity.
To address transaction size limitations, Solana introduced a new transaction format supporting multiple versions. Currently, two transaction versions are supported:
legacy
- The original transaction format0
- The latest format, which supports Address Lookup Tables.
Existing Solana programs do not require changes to support versioned transactions. However, client-side code created prior to their introduction should be updated. In this lesson, we'll cover the basics of versioned transactions and how to use them, including:
- Creating versioned transactions
- Creating and managing lookup tables
- Using lookup tables in versioned transactions
Versioned Transactions #
In Solana transactions, one of the largest space consumers is account addresses, which are 32 bytes each. For transactions with 39 accounts, the size limit is exceeded even before accounting for instruction data. Typically, transactions become too large with around 20 accounts.
Versioned transactions address this issue by introducing Address Lookup Tables, which allow addresses to be stored separately and referenced via a 1-byte index. This greatly reduces transaction size by minimizing the space needed for account addresses.
Even if Address Lookup Tables are not required for your use case, understanding
versioned transactions is crucial for maintaining compatibility with the latest
Solana features. The @solana/web3.js
library provides all necessary tools to
work with versioned transactions and lookup tables.
Create versioned transactions #
To create a versioned transaction, you first create a TransactionMessage
with
the following parameters:
payerKey
- the public key of the account that will pay for the transactionrecentBlockhash
- a recent blockhash from the networkinstructions
- the instructions to be executed in the transaction.
Once the message object is created, you can convert it into a version 0
transaction using the compileToV0Message()
method.
import * as web3 from "@solana/web3.js";
// Example transfer instruction
const transferInstruction = SystemProgram.transfer({
fromPubkey: payer.publicKey, // Public key of the sender account
toPubkey: toAccount.publicKey, // Public key of the receiver account
lamports: 1 * LAMPORTS_PER_SOL, // Amount to transfer in lamports
});
// Get the latest blockhash
const { blockhash } = await connection.getLatestBlockhash();
// Create the transaction message
const message = new TransactionMessage({
payerKey: payer.publicKey, // Public key of the payer account
recentBlockhash: blockhash, // Most recent blockhash
instructions: [transferInstruction], // Transaction instructions
}).compileToV0Message();
Next, pass the compiled message into the VersionedTransaction
constructor to
create a versioned transaction. The transaction is then signed and sent to the
network, similar to how legacy transactions are handled.
// Create the versioned transaction from the compiled message
const transaction = new VersionedTransaction(message);
// Sign the transaction with the payer's keypair
transaction.sign([payer]);
// Send the signed transaction to the network
const signature = await connection.sendTransaction(transaction);
Address Lookup Table #
Address Lookup Tables (LUTs) are accounts that store references to other account addresses. These LUT accounts, owned by the Address Lookup Table Program, increase the number of accounts that can be included in a transaction.
In versioned transactions, LUT addresses are included, and additional accounts are referenced with a 1-byte index instead of the full 32-byte address, reducing space used by the transaction.
The @solana/web3.js
library offers an AddressLookupTableProgram
class,
providing methods to manage LUTs:
createLookupTable
- creates a new LUT account.freezeLookupTable
- makes a LUT immutable.extendLookupTable
- adds addresses to an existing LUT.deactivateLookupTable
- begins the deactivation period for an LUT.closeLookupTable
- permanently closes an LUT account.
Create a lookup table #
You can use the createLookupTable
method to construct the instruction for
creating a lookup table. This requires the following parameters:
authority
- the account authorized to modify the lookup table.payer
- the account responsible for paying the account creation fees.recentSlot
- a recent slot used to derive the lookup table's address.
The function returns both the instruction for creating the LUT and its address.
// Get the current slot
const slot = await connection.getSlot();
// Create the lookup table creation instruction and retrieve its address
const [lookupTableInst, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: user.publicKey, // Account authorized to modify the LUT
payer: user.publicKey, // Account paying for transaction fees
recentSlot: slot - 1, // Use a recent slot to derive the LUT address
});
Under the hood, the lookup table address is a Program Derived Address (PDA)
generated using the authority
and recentSlot
as seeds.
const [lookupTableAddress, bumpSeed] = PublicKey.findProgramAddressSync(
[params.authority.toBuffer(), toBufferLE(BigInt(params.recentSlot), 8)],
this.programId,
);
Using the most recent slot sometimes results in errors when submitting the
transaction. To avoid this, it’s recommended to use a slot that is one slot
before the most recent one (recentSlot: currentSlot - 1
). If you still
encounter errors when sending the transaction, try resubmitting it.
"Program AddressLookupTab1e1111111111111111111111111 invoke [1]",
"188115589 is not a recent slot",
"Program AddressLookupTab1e1111111111111111111111111 failed: invalid instruction data";
Extend a lookup table #
The extendLookupTable
method creates an instruction to add addresses to an
existing lookup table. It requires the following parameters:
payer
- the account responsible for paying transaction fees and any additional rent.authority
- the account authorized to modify the lookup table.lookupTable
- the address of the lookup table to be extended.addresses
- the list of addresses to add to the lookup table.
The function returns an instruction to extend the lookup table.
const addresses = [
new PublicKey("31Jy3nFeb5hKVdB4GS4Y7MhU7zhNMFxwF7RGVhPc1TzR"),
new PublicKey("HKSeapcvwJ7ri6mf3HwBtspLFTDKqaJrMsozdfXfg5y2"),
// Add more addresses here
];
// Create the instruction to extend the lookup table with the provided addresses
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
payer: user.publicKey, // Account paying for transaction fees
authority: user.publicKey, // Account authorized to modify the lookup table
lookupTable: lookupTableAddress, // Address of the lookup table to extend
addresses: addresses, // Addresses to add to the lookup table
});
Note that when extending a lookup table, the number of addresses that can be added in a single instruction is limited by the transaction size limit of 1232 bytes. You can add approximately 30 addresses in one transaction. If you need to add more than that, multiple transactions are required. Each lookup table can store up to 256 addresses.
Send Transaction #
After creating the instructions, you can add them to a transaction and send it to the network:
// Get the latest blockhash
const { blockhash } = await connection.getLatestBlockhash();
// Create the transaction message
const message = new TransactionMessage({
payerKey: payer.publicKey, // Account paying for the transaction
recentBlockhash: blockhash, // Latest blockhash
instructions: [lookupTableInst, extendInstruction], // Instructions to be included in the transaction
}).compileToV0Message();
// Create the versioned transaction from the message
const transaction = new VersionedTransaction(message);
// Sign the transaction
transaction.sign([payer]);
// Send the signed transaction to the network
const transactionSignature = await connection.sendTransaction(transaction);
Note that after you create or extend a lookup table, it must "warm up" for one slot before the lookup table or newly added addresses can be used in transactions. You can only access lookup tables and addresses added in slots prior to the current one.
If you encounter the following error, it may indicate that you're trying to access a lookup table or an address before the warm-up period has completed:
SendTransactionError: failed to send transaction: invalid transaction: Transaction address table lookup uses an invalid index
To avoid this issue, ensure you add a delay after extending the lookup table before attempting to reference the table in a transaction.
Deactivate a lookup table #
When a lookup table (LUT) is no longer needed, you can deactivate it to reclaim its rent balance. Deactivating a LUT puts it into a "cool-down" period (approximately 513 slots) during which it can still be used by transactions. This prevents transactions from being censored by deactivating and recreating LUTs within the same slot.
To deactivate a LUT, use the deactivateLookupTable
method with the following
parameters:
lookupTable
- the address of the lookup table to be deactivated.authority
- the account with the authority to deactivate the LUT.
const deactivateInstruction = AddressLookupTableProgram.deactivateLookupTable({
lookupTable: lookupTableAddress, // Address of the lookup table to deactivate
authority: user.publicKey, // Authority to modify the lookup table
});
Close a lookup table #
Once a LUT has been deactivated and the cool-down period has passed, you can
close the lookup table to reclaim its rent balance. Use the closeLookupTable
method, which requires the following parameters:
lookupTable
- the address of the LUT to be closed.authority
- the account with the authority to close the LUT.recipient
- the account that will receive the reclaimed rent balance.
const closeInstruction = AddressLookupTableProgram.closeLookupTable({
lookupTable: lookupTableAddress, // Address of the lookup table to close
authority: user.publicKey, // Authority to close the LUT
recipient: user.publicKey, // Recipient of the reclaimed rent balance
});
Attempting to close a LUT before it has been fully deactivated will result in the following error:
"Program AddressLookupTab1e1111111111111111111111111 invoke [1]",
"Table cannot be closed until it's fully deactivated in 513 blocks",
"Program AddressLookupTab1e1111111111111111111111111 failed: invalid program argument";
Freeze a lookup table #
In addition to standard CRUD operations, you can "freeze" a lookup table. This makes it immutable so that it can no longer be extended, deactivated, or closed.
The freezeLookupTable
method is used for this operation and takes the
following parameters:
lookupTable
- the address of the LUT to freeze.authority
- the account with the authority to freeze the LUT.
const freezeInstruction = AddressLookupTableProgram.freezeLookupTable({
lookupTable: lookupTableAddress, // Address of the lookup table to freeze
authority: user.publicKey, // Authority to freeze the LUT
});
Once a LUT is frozen, any attempt to modify it will result in an error like the following:
"Program AddressLookupTab1e1111111111111111111111111 invoke [1]",
"Lookup table is frozen",
"Program AddressLookupTab1e1111111111111111111111111 failed: Account is immutable";
Using lookup tables in versioned transactions #
To utilize a lookup table in a versioned transaction, first retrieve the lookup table account using its address:
// Fetch the lookup table account from the blockchain using its address
const lookupTableAccount = (
await connection.getAddressLookupTable(new PublicKey(lookupTableAddress))
).value;
Once you have the lookup table account, you can create the list of instructions
for the transaction. When constructing the TransactionMessage
, pass the lookup
table accounts as an array to the compileToV0Message()
method. You can include
multiple lookup table accounts if needed.
const message = new web3.TransactionMessage({
payerKey: payer.publicKey, // Public key of the account paying for the transaction
recentBlockhash: blockhash, // Blockhash of the most recent block
instructions: instructions, // Instructions to be included in the transaction
}).compileToV0Message([lookupTableAccount]); // Include lookup table accounts
// Create a versioned transaction using the compiled message
const transaction = new web3.VersionedTransaction(message);
// Sign the transaction
transaction.sign([payer]);
// Send the signed transaction to the network
const transactionSignature = await connection.sendTransaction(transaction);
Lab #
Let's go ahead and practice using lookup tables!
This lab will guide you through creating, extending, and using a lookup table in a versioned transaction.
1. Create the try-large-transaction.ts
file #
To begin, create a new file named try-large-transaction.ts
in your project
directory. This file will contain the code to illustrate a scenario where a
legacy transaction is created to transfer SOL to 22 recipients in a single
atomic transaction. The transaction will include 22 separate instructions, each
transferring SOL from the payer (signer) to a different recipient.
This example highlights a key limitation of legacy transactions when trying to accommodate many account addresses within a single transaction. As expected, when attempting to send this transaction, it will likely fail due to exceeding the transaction size limits.
Here’s the code to include in try-large-transaction.ts
:
import {
Connection,
clusterApiUrl,
Keypair,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
initializeKeypair,
makeKeypairs,
getExplorerLink,
} from "@solana-developers/helpers";
import dotenv from "dotenv";
dotenv.config();
async function main() {
// Connect to the local Solana cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// Initialize the keypair from the environment variable or create a new one
const payer = await initializeKeypair(connection);
// Generate 22 recipient keypairs using makeKeypairs
const recipients = makeKeypairs(22).map(keypair => keypair.publicKey);
// Create a legacy transaction
const transaction = new Transaction();
// Add 22 transfer instructions to the transaction
recipients.forEach(recipient => {
transaction.add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: recipient,
lamports: LAMPORTS_PER_SOL * 0.01, // Transfer 0.01 SOL to each recipient
}),
);
});
// Sign and send the transaction
try {
const signature = await sendAndConfirmTransaction(connection, transaction, [
payer,
]);
console.log(
`Transaction successful with signature: ${getExplorerLink("tx", signature, "devnet")}`,
);
} catch (error) {
console.error("Transaction failed:", error);
}
}
To run the example, execute npx esrun try-large-transaction.ts
. This process
will:
- Generate a new keypair.
- Store the keypair details in the
.env
file. - Request airdrop of devnet SOL to the generated keypair.
- Attempt to send the transaction.
- Since the transaction includes 22 instructions, it is expected to fail with the error: "Transaction too large".
Creating .env file
Current balance is 0
Airdropping 1 SOL...
New balance is 1
PublicKey: 7YsGYC4EBs6Dxespe4ZM3wfCp856xULWoLw7QUcVb6VG
Error: Transaction too large: 1244 > 1232
2. Create the use-lookup-tables.ts
File #
Next, we'll explore how to use lookup tables in combination with versioned transactions to overcome the limitation of legacy transactions and include a greater number of addresses in a single transaction.
Create a new file named use-lookup-tables.ts
in your project directory. This
file will contain the code to demonstrate the use of lookup tables.
Here’s the starter code to include in use-lookup-tables.ts
file:
import {
Connection,
clusterApiUrl,
Keypair,
TransactionInstruction,
AddressLookupTableAccount,
SystemProgram,
VersionedTransaction,
TransactionMessage,
AddressLookupTableProgram,
LAMPORTS_PER_SOL,
getSlot,
} from "@solana/web3.js";
import {
initializeKeypair,
makeKeypairs,
getExplorerLink,
} from "@solana-developers/helpers";
import dotenv from "dotenv";
dotenv.config();
async function main() {
// Connect to the local Solana cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// Initialize the keypair from the environment variable or create a new one
const payer = await initializeKeypair(connection);
// Generate 22 recipient keypairs using makeKeypairs
const recipients = makeKeypairs(22).map(keypair => keypair.publicKey);
}
Next, we will create a few helper functions that will be crucial for working with versioned transactions and lookup tables. These functions will simplify our process and make our code more modular and reusable.
3. Create a sendV0Transaction
helper function #
To handle versioned transactions, we will create a helper function in
use-lookup-tables.ts
file, called sendV0Transaction
, to simplify the
process. This function will accept the following parameters:
connection
: the solana connection to the cluster (e.g., devnet).user
: the keypair of the user (payer) signing the transaction.instructions
: an array of TransactionInstruction objects to include in the transaction.lookupTableAccounts
(optional): an array of lookup table accounts, if applicable, to reference additional addresses.
This helper function will:
- Retrieve the latest blockhash and last valid block height from the Solana network.
- Compile a versioned transaction message using the provided instructions.
- Sign the transaction using the user's keypair.
- Send the transaction to the network.
- Confirm the transaction and log the transaction's URL using Solana Explorer.
async function sendV0Transaction(
connection: Connection,
user: Keypair,
instructions: TransactionInstruction[],
lookupTableAccounts?: AddressLookupTableAccount[],
) {
// Get the latest blockhash and last valid block height
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
// Create a new transaction message with the provided instructions
const messageV0 = new TransactionMessage({
payerKey: user.publicKey, // The payer (i.e., the account that will pay for the transaction fees)
recentBlockhash: blockhash, // The blockhash of the most recent block
instructions, // The instructions to include in the transaction
}).compileToV0Message(lookupTableAccounts);
// Create a versioned transaction from the message
const transaction = new VersionedTransaction(messageV0);
// Use the helper function to send and confirm the transaction
const txid = await sendAndConfirmTransactionV0(
connection,
transaction,
[user],
{
commitment: "finalized", // Ensures the transaction is confirmed at the highest level
},
);
// Log the transaction URL on the Solana Explorer using the helper
const explorerLink = getExplorerLink("tx", txid, "devnet");
console.log(
`Transaction successful! View it on Solana Explorer: ${explorerLink}`,
);
}
4. Create a waitForNewBlock
helper function #
When working with lookup tables, it's important to remember that newly created or extended lookup tables cannot be referenced immediately. Therefore, before submitting transactions that reference these tables, we need to wait for a new block to be generated.
We will create a waitForNewBlock
helper function that accepts:
connection
: the Solana network connection.targetBlockHeight
: the target block height to wait for.
This function will:
- Start an interval that checks the current block height of the network every second (1000ms).
- Resolve the promise once the current block height exceeds the target block height.
async function waitForNewBlock(
connection: Connection,
targetHeight: number,
): Promise<void> {
console.log(`Waiting for ${targetHeight} new blocks...`);
// Get the initial block height of the blockchain
const { lastValidBlockHeight: initialBlockHeight } =
await connection.getLatestBlockhash();
return new Promise(resolve => {
const SECOND = 1000;
const checkInterval = 1 * SECOND; // Interval to check for new blocks (1000ms)
// Set an interval to check for new block heights
const intervalId = setInterval(async () => {
try {
// Get the current block height
const { lastValidBlockHeight: currentBlockHeight } =
await connection.getLatestBlockhash();
// If the current block height exceeds the target, resolve and clear interval
if (currentBlockHeight >= initialBlockHeight + targetHeight) {
clearInterval(intervalId);
console.log(`New block height reached: ${currentBlockHeight}`);
resolve();
}
} catch (error) {
console.error("Error fetching block height:", error);
clearInterval(intervalId);
resolve(); // Resolve to avoid hanging in case of errors
}
}, checkInterval);
});
}
5. Create an initializeLookupTable
function #
Next, we need to initialize a lookup table to hold the addresses of the
recipients. The initializeLookupTable
function will accept the following
parameters:
user
: the user's keypair (payer and authority).connection
: the Solana network connection.addresses
: an array of recipient addresses (public keys) to add to the lookup table.
The function will:
- Retrieve the current slot to derive the lookup table's address.
- Generate the necessary instructions to create and extend the lookup table with the provided recipient addresses.
- Send and confirm a transaction that includes these instructions.
- Return the address of the newly created lookup table.
Although the transaction includes the full recipient addresses, using the lookup table allows Solana to reference those addresses with significantly fewer bytes in the actual transaction. By including the lookup table in the versioned transaction, the framework optimizes the transaction size, replacing addresses with pointers to the lookup table.
This design is crucial for enabling the transaction to support more recipients by staying within Solana’s transaction size limits.
async function initializeLookupTable(
user: Keypair,
connection: Connection,
addresses: PublicKey[],
): Promise<PublicKey> {
// Get the current slot using a helper function from @solana/web3.js
const slot = await getSlot(connection);
// Create an instruction for creating a lookup table
// and retrieve the address of the new lookup table
const [lookupTableInst, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: user.publicKey, // The authority to modify the lookup table
payer: user.publicKey, // The payer for transaction fees
recentSlot: slot - 1, // The slot for lookup table address derivation
});
console.log("Lookup Table Address:", lookupTableAddress.toBase58());
// Create an instruction to extend a lookup table with the provided addresses
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
payer: user.publicKey, // The payer of transaction fees
authority: user.publicKey, // The authority to extend the lookup table
lookupTable: lookupTableAddress, // Address of the lookup table to extend
addresses: addresses.slice(0, 30), // Add up to 30 addresses per instruction
});
// Use the helper function to send a versioned transaction
await sendVersionedTransaction(connection, user, [
lookupTableInst,
extendInstruction,
]);
return lookupTableAddress;
}
6. Modify main
to use lookup tables #
With the helper functions in place, we are now ready to modify the main
function to utilize versioned transactions and address lookup tables. To do so,
we will follow these steps:
- Call
initializeLookupTable
: Create and extend the lookup table with the recipients' addresses. - Call
waitForNewBlock
: Ensure the lookup table is activated by waiting for a new block. - Retrieve the Lookup Table: Use
connection.getAddressLookupTabl
e to fetch the lookup table and reference it in the transaction. - Create Transfer Instructions: Generate a transfer instruction for each recipient.
- Send the Versioned Transaction: Use
sendV0Transaction
to send a single transaction with all transfer instructions, referencing the lookup table.
async function main() {
// Connect to the devnet Solana cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// Initialize the keypair from the environment variable or create a new one
const payer = await initializeKeypair(connection);
// Generate 22 recipient keypairs using makeKeypairs
const recipients = makeKeypairs(22).map(keypair => keypair.publicKey);
// Initialize the lookup table with the generated recipients
const lookupTableAddress = await initializeLookupTable(
user,
connection,
recipients,
);
// Wait for a new block before using the lookup table
await waitForNewBlock(connection, 1);
// Fetch the lookup table account
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value;
// Check if the lookup table was successfully fetched
if (!lookupTableAccount) {
throw new Error("Lookup table not found");
}
// Create transfer instructions for each recipient
const transferInstructions = recipients.map(recipient =>
SystemProgram.transfer({
fromPubkey: user.publicKey, // The payer
toPubkey: recipient, // The recipient
lamports: LAMPORTS_PER_SOL * 0.01, // Amount to transfer
}),
);
// Send the versioned transaction including the lookup table
const txid = await sendVersionedTransaction(
connection,
user,
transferInstructions,
[lookupTableAccount],
);
// Log the transaction link for easy access
console.log(`Transaction URL: ${getExplorerLink("tx", txid, "devnet")}`);
}
Even though we will create transfer instructions with full recipient addresses,
the use of lookup tables allows the @solana/web3.js
framework to optimize the
transaction size. The addresses in the transaction that match entries in the
lookup table will be replaced with compact pointers referencing the lookup
table. By doing this, addresses will be represented using only a single byte in
the final transaction, significantly reducing the transaction's size.
Use npx esrun use-lookup-tables.ts
in the command line to execute the main
function. You should see an output similar to the following:
Current balance is 1.38866636
PublicKey: 8iGVBt3dcJdp9KfyTRcKuHY6gXCMFdnSG2F1pAwsUTMX
lookup table address: Cc46Wp1mtci3Jm9EcH35JcDQS3rLKBWzy9mV1Kkjjw7M
https://explorer.solana.com/tx/4JvCo2azy2u8XK2pU8AnJiHAucKTrZ6QX7EEHVuNSED8B5A8t9GqY5CP9xB8fZpTNuR7tbUcnj2MiL41xRJnLGzV?cluster=devnet
Waiting for 1 new blocks
https://explorer.solana.com/tx/rgpmxGU4QaAXw9eyqfMUqv8Lp6LHTuTyjQqDXpeFcu1ijQMmCH2V3Sb54x2wWAbnWXnMpJNGg4eLvuy3r8izGHt?cluster=devnet
Finished successfully
The first transaction link in the console represents the transaction for creating and extending the lookup table. The second transaction represents the transfers to all recipients. Feel free to inspect these transactions in the explorer.
Remember, this same transaction was failing when you first downloaded the starter code. Now that we're using lookup tables, we can do all 22 transfers in a single transaction.
6. Add more addresses to the lookup table #
Keep in mind that the solution we've come up with so far only supports transfers to up to 30 accounts since we only extend the lookup table once. When you factor in the transfer instruction size, it's actually possible to extend the lookup table with an additional 27 addresses and complete an atomic transfer to up to 57 recipients. Let's go ahead and add support for this now!
All we need to do is go into initializeLookupTable
and do two things:
- Modify the existing call to
extendLookupTable
to only add the first 30 addresses (any more than that and the transaction will be too large) - Add a loop that will keep extending a lookup table of 30 addresses at a time until all addresses have been added
async function initializeLookupTable(
user: Keypair,
connection: Connection,
addresses: PublicKey[],
): Promise<PublicKey> {
// Get the current slot
const slot = await connection.getSlot();
// Create the lookup table and retrieve its address
const [lookupTableInst, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: user.publicKey, // The authority to modify the lookup table
payer: user.publicKey, // The payer for the transaction fees
recentSlot: slot - 1, // Recent slot to derive lookup table's address
});
console.log("Lookup table address:", lookupTableAddress.toBase58());
// Helper function to extend the lookup table in batches
const extendLookupTable = async (remainingAddresses: PublicKey[]) => {
while (remainingAddresses.length > 0) {
const toAdd = remainingAddresses.slice(0, 30); // Add up to 30 addresses
remainingAddresses = remainingAddresses.slice(30);
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
payer: user.publicKey,
authority: user.publicKey,
lookupTable: lookupTableAddress,
addresses: toAdd,
});
// Send the transaction to extend the lookup table with the new addresses
await sendVersionedTransaction(connection, user, [extendInstruction]);
}
};
// Send the initial transaction to create the lookup table and add the first 30 addresses
const initialBatch = addresses.slice(0, 30);
const remainingAddresses = addresses.slice(30);
await sendVersionedTransaction(connection, user, [lookupTableInst]);
// Extend the lookup table with the remaining addresses, if any
await extendLookupTable(initialBatch);
await extendLookupTable(remainingAddresses);
return lookupTableAddress;
}
Congratulations! If you feel good about this lab, you're probably ready to work with lookup tables and versioned transactions on your own. If you want to take a look at the final solution code you can find it on the solution branch.
Challenge #
As a challenge, experiment with deactivating, closing, and freezing lookup tables. Remember that you need to wait for a lookup table to finish deactivating before you can close it. Also, if a lookup table is frozen, it cannot be modified (deactivated or closed), so you will have to test separately or use separate lookup tables.
- Create a function for deactivating the lookup table.
- Create a function for closing the lookup table
- Create a function for freezing the lookup table
- Test the functions by calling them in the
main()
function
You can reuse the functions we created in the lab for sending the transaction and waiting for the lookup table to activate/deactivate. Feel free to reference this solution code.
Push your code to GitHub and tell us what you thought of this lesson!