閱讀時間 16 分鐘
概覽
On October 10, 2022 (Epoch 358), Solana added support for new transaction version types through a concept known as "Versioned Transactions". After this change, the Solana runtime now supports two types of transactions: "legacy" (older transactions) and "0" (transactions that include Address Lookup Tables). Address lookup tables bring a new way for developers to efficiently load many addresses into a transaction, so if you have had issues with storage size due to accounts, this could help you out!
The Address Lookup Table Program (Program ID: AddressLookupTab1e1111111111111111111111111) allows you to store public keys in on-chain lookup tables and call the Lookup Table in your Versioned Transaction. Because serialized transactions transmitted to Solana validators must not exceed 1,232 bytes (Source Code, Reference), leveraging a Lookup Table can reduce transaction size and enable more complex transaction instructions (e.g., more accounts, more integrated Cross-Program Invocations, etc.). Solana lookup tables "effectively 'compress' a 32-byte address into a 1-byte index value" (Source). This means by using lookup tables, our transactions sizes will be smaller (or that we can pack more into our transactions)!
您將負責的工作內容
在本指南中,您將:
- create and execute a Version 0 (V0) Transaction,
- create and populate an Address Lookup Table, and
- compare the transaction size of two nearly identical transactions (one using a lookup table and one without).
If you need help with ensuring your existing client-side apps can support Versioned Transactions, check out our *Guide: How to Update Your Solana Client to Handle Versioned Transactions.
*
您需要準備的物品
- 已安裝Node.js(版本 16.15 或更高)
- 已具備 TypeScript 經驗,並已安裝ts-node
- 具備執行基本 Solana 交易的經驗(指南:如何使用 JavaScript 在 Solana 上發送交易)
If you're coming over from our previous Guide: How to Use Versioned Transactions on Solana, you can reuse your app.ts and can Skip to the Assemble a Version 0 Transaction section. Otherwise, set up your project:
設定您的專案
Create a new project directory in your terminal with the following:
mkdir solana-versioned-tx
cd solana-versioned-tx
為您的應用程式建立一個名為app.ts 的檔案:
echo > app.ts
請使用「yes」參數初始化您的專案,以便為新套件採用預設值:
yarn init --yes
#或
npm init --yes
建立一個已啟用 .json 匯入功能的tsconfig.json檔案:
tsc -init --resolveJsonModule true
安裝 Solana Web3 依賴項
在這個練習中,我們需要加入 Solana Web3 函式庫。請在終端機中輸入:
yarn add @solana/web3.js@1
#或
npm install @solana/web3.js@1
我們開始吧。
設定您的應用程式
匯入必要的依賴項
開啟app.ts 檔案,並將以下導入語句貼到第 1 行:
import { AddressLookupTableProgram, Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, TransactionSignature, TransactionConfirmationStatus, SignatureStatus } from '@solana/web3.js';
We are importing a few essential methods and classes from the Solana Web3 library. There are probably a couple of imports you have not seen before (e.g., AddressLookupTableProgram and VersionedTransaction)--we will cover those later in this guide.
建立錢包並領取 SOL 空投
我們首先需要完成的任務,是建立一個附帶錢包的帳戶並為其充值。我們將使用下方的便捷工具,自動生成一個新錢包,並向其中空投 1 SOL。(您也可以透過 Keypair.generate() 以及 requestAirdrop() (若您偏好更為手動的方式,也可使用這些函式)。
注意:若過於頻繁地發送空投請求,可能會觸發 429(請求過多)錯誤。
成功產生金鑰對後,您會發現有兩個新的常數: 秘密 以及 SIGNER_WALLET, 一組金鑰對。該 秘密 是一個 32 位元組的陣列,用於產生公鑰和私鑰。該 SIGNER_WALLET 這是一個用於簽署交易的 Keypair 實例(我們已空投了一些開發網 SOL 來支付 gas 費用)。若您尚未將其加入程式碼中,請務必在其他常數下方將其加入。
在您的 `import` 語句下方,貼上新的密鑰,並加入:
const secret = [0,...,0]; // 👈 Replace with your secret
const SIGNER_WALLET = Keypair.fromSecretKey(new Uint8Array(secret));
const DESTINATION_WALLET = Keypair.generate();
//const LOOKUP_TABLE_ADDRESS = new PublicKey(""); // We will add this later
我們已定義了兩個錢包: SIGNER_WALLET 將把 SOL 發送至我們的 DESTINATION_WALLET. 我們還新增了一個常數, LOOKUP_TABLE_ADDRESS,我們稍後會更新此處,使其引用我們的查詢表中的鏈上帳號 ID。
設定您的 Quicknode 端點
若要在 Solana 上進行開發,您需要一個 API 端點來連線至該網路。您可以使用公開節點,或自行部署並管理基礎架構;不過,若您希望將回應時間縮短 8 倍,不妨將繁重的工作交給我們處理。
了解為何超過 50% 的 Solana 專案都選擇 Quicknode,並在此註冊帳號。我們將使用一個 Solana Devnet 節點。
複製 HTTP 提供者連結:

在app.ts檔案中,於 import 語句下方,宣告您的 RPC 並建立與 Solana 的連線:
const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const SOLANA_CONNECTION = new Connection(QUICKNODE_RPC);
您的環境應如下所示:

好啦,讓我們開始打造吧!
Assemble a Version 0 Transaction
To use Lookup Tables, you must use Version 0 transactions. If you're unfamiliar with constructing and executing Versioned Transactions, check out our guide, here.
Let's start by creating a new function, createAndSendV0Tx, that accepts an array of TransactionInstructions, txInstructions:
async function createAndSendV0Tx(txInstructions: TransactionInstruction[]) {
// Step 1 - Fetch Latest Blockhash
let latestBlockhash = await SOLANA_CONNECTION.getLatestBlockhash('finalized');
console.log(" ✅ - Fetched latest blockhash. Last valid height:", latestBlockhash.lastValidBlockHeight);
// Step 2 - Generate Transaction Message
const messageV0 = new TransactionMessage({
payerKey: SIGNER_WALLET.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: txInstructions
}).compileToV0Message();
console.log(" ✅ - Compiled transaction message");
const transaction = new VersionedTransaction(messageV0);
// Step 3 - Sign your transaction with the required `Signers`
transaction.sign([SIGNER_WALLET]);
console.log(" ✅ - Transaction Signed");
// Step 4 - Send our v0 transaction to the cluster
const txid = await SOLANA_CONNECTION.sendTransaction(transaction, { maxRetries: 5 });
console.log(" ✅ - Transaction sent to network");
// Step 5 - Confirm Transaction
const confirmation = await confirmTransaction(SOLANA_CONNECTION, txid);
if (confirmation.value.err) { throw new Error(" ❌ - Transaction not confirmed.") }
console.log('🎉 Transaction succesfully confirmed!', '\n', `https://explorer.solana.com/tx/${txid}?cluster=devnet`);
}
Let's walk through our code:
- Step 1: We fetch the latest blockhash from the network. Note: We pass the parameter, 'finalized', to make sure the block does not belong to a dropped fork.
- Step 2: Using our txInstructions parameter and the latestBlockhash, we create a new MessageV0 by building a Message and executing the .compileToV0Message() method.
- Step 3: We sign the transaction with an array of signers. In this case, it is just our SIGNER_WALLET.
- Step 4: We send the transaction to the cluster using sendTransaction, which will return a transaction signature/id.
- Step 5: We wait for the cluster to confirm the transaction has succeeded. If it succeeds, we log our explorer URL; otherwise, we throw an error.
Let's define the confirmTransaction function we called in our createAndSendV0Tx 函式。請將以下程式碼加入您的 app.ts 檔案:
async function confirmTransaction(
connection: Connection,
signature: TransactionSignature,
desiredConfirmationStatus: TransactionConfirmationStatus = 'confirmed',
timeout: number = 30000,
pollInterval: number = 1000,
searchTransactionHistory: boolean = false
): Promise<SignatureStatus> {
const start = Date.now();
while (Date.now() - start < timeout) {
const { value: statuses } = await connection.getSignatureStatuses([signature], { searchTransactionHistory });
if (!statuses || statuses.length === 0) {
throw new Error('Failed to get signature status');
}
const status = statuses[0];
if (status === null) {
// If status is null, the transaction is not yet known
await new Promise(resolve => setTimeout(resolve, pollInterval));
continue;
}
if (status.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
}
if (status.confirmationStatus && status.confirmationStatus === desiredConfirmationStatus) {
return status;
}
if (status.confirmationStatus === 'finalized') {
return status;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
}
這僅會持續查詢 Solana 網路以獲取交易狀態,直到交易獲得確認或達到超時為止。我們已為超時時間和查詢間隔設定了一些預設值,但您可以根據需要進行調整。
Alright! We're all set. Let's add some instructions.
Create an Address Lookup Table
Create a new async function, createLookupTable, that will build our transaction instruction and invoke createAndSendV0Tx:
async function createLookupTable() {
// Step 1 - Get a lookup table address and create lookup table instruction
const [lookupTableInst, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: SIGNER_WALLET.publicKey,
payer: SIGNER_WALLET.publicKey,
recentSlot: await SOLANA_CONNECTION.getSlot(),
});
// Step 2 - Log Lookup Table Address
console.log("Lookup Table Address:", lookupTableAddress.toBase58());
// Step 3 - Generate a transaction and send it to the network
createAndSendV0Tx([lookupTableInst]);
}
Breaking down our code:
- Step 1: We create two variables, lookupTableInst and lookupTableAddress, by destructuring the results of the createLookupTable method. This method returns the public key for the table once created and a TransactionInstruction that can be passed into our createAndSendV0Tx function.
- Step 2: We log the table's address (which we will need later in this exercise).
- Step 3: Finaly, we call createAndSendV0Tx by passing lookupTableInst inside of an array to match our type requirements.
Awesome! At this point, you should be able to run your code and create an empty lookup table. After your function, call it by adding:
createLookupTable();
Then in your terminal enter:
ts-node app.ts
You should see your transaction progressing in your terminal and ultimately receive a URL to your transaction page on Solana Explorer:

Our lookup table account address is: 3uBhgRWPTPLfvfqxi4M9eVZC8nS1kDG9XPkdHKgG69nw:

Great Job! You have made your first lookup table.
Before we move on, let's do some cleanup:
- Remove your call to createLookupTable(). We won't need it again.
- Remember the LOOKUP_TABLE_ADDRESS constant we created a while ago? Remove the comment backslashes, and add your table lookup address from your console to your PublicKey declaration like so (this is line 6 for us):
const LOOKUP_TABLE_ADDRESS = new PublicKey("YOUR_TABLE_ADDRESS_HERE");
// e.g., const LOOKUP_TABLE_ADDRESS = new PublicKey("3uBhgRWPTPLfvfqxi4M9eVZC8nS1kDG9XPkdHKgG69nw");
Add Addresses to Your Lookup Table
Since we have already created createAndSendV0Tx, adding an address to a lookup table is easy! All we need to do is create a TransactionInstruction. Create a new async function, addAddressesToTable that uses the AddressLookupTableProgram.extendLookupTable() method:
async function addAddressesToTable() {
// Step 1 - Create Transaction Instruction
const addAddressesInstruction = AddressLookupTableProgram.extendLookupTable({
payer: SIGNER_WALLET.publicKey,
authority: SIGNER_WALLET.publicKey,
lookupTable: LOOKUP_TABLE_ADDRESS,
addresses: [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey
],
});
// Step 2 - Generate a transaction and send it to the network
await createAndSendV0Tx([addAddressesInstruction]);
console.log(`Lookup Table Entries: `,`https://explorer.solana.com/address/${LOOKUP_TABLE_ADDRESS.toString()}/entries?cluster=devnet`)
}
Let's break down what's going on with the extendLookupTable method:
- We pass our SIGNER_WALLET to pay the transaction fees and any additional rent incurred.
- We define our update authority - in our case, we set that as the SIGNER_WALLET in our table creation step above.
- We pass in the lookup table account address (which we defined as LOOKUP_TABLE_ADDRESS).
- We pass an array of addresses into our lookup table. We will pass in a few random public keys, but you can pass in any public key that you like! The Program's "compression" supports storing up to 256 addresses in a single lookup table!
- Let's also log a link to our lookup table entries for easy access after the transaction is complete. Finally, we pass our TransactionInstruction into createAndSendV0Tx to generate a transaction and send it to the network!
After your function, call your new function by adding:
addAddressesToTable();
Run your code--in your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.
You should see a similar transaction flow in your terminal and success with a link to your transaction and your lookup table entries on Solana Explorer. Go to the lookup table entries. You should see a list of all of your table's stored public keys:

Nice work! You can modify your addresses array and rerun it to add additional public keys to your lookup table.
Before moving on, delete your call to addAddressesToTable();. We won't need it again.
Lookup Addresses in Your Lookup Table
Okay, so if you're following along, you should have an Address Lookup Table populated with a few addresses. Now, we are going to fetch all of our addresses and log them to your terminal. Create a new function, findAddressesInTable:
async function findAddressesInTable() {
// Step 1 - Fetch our address lookup table
const lookupTableAccount = await SOLANA_CONNECTION.getAddressLookupTable(LOOKUP_TABLE_ADDRESS)
console.log(`Successfully found lookup table: `, lookupTableAccount.value?.key.toString());
// Step 2 - Make sure our search returns a valid table
if (!lookupTableAccount.value) return;
// Step 3 - Log each table address to console
for (let i = 0; i < lookupTableAccount.value.state.addresses.length; i++) {
const address = lookupTableAccount.value.state.addresses[i];
console.log(` Address ${(i + 1)}: ${address.toBase58()}`);
}
}
Here's a summary of what we are doing here:
- Fetch our lookup table using a method called getAddressLookupTable() and pass our lookup table's account address. If successful, the query should return an AddressLookupTableAccount object.
- Stop the function if we don't find a valid lookup table.
- Iterate through each address found in our lookup table and log it to the console.
Not bad, right?
Call your new function and then run it:
findAddressesInTable();
In your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.
You should see your list of addresses like this:

If you're not seeing that or having trouble with any of the code, feel free to drop us a line on Discord -- we're here to help!
Remove or comment out your call to findAddressesInTable(); before moving on.
Put Lookup Tables to the Test
You're well on your way to becoming a Solana lookup table master--give yourself a quick pat on the back for making it this far. But what's the point of all of this? Good question! Solana lookup tables "effectively 'compress' a 32-byte address into a 1-byte index value" (Source). This means by using lookup tables, our transactions sizes will be smaller (or that we can pack more into our transactions)--let's prove it!
We're going to create a function that generates two transactions using the same transfer instruction; in one of them, we will pass our lookup table, and in the other we will not. We will check each transaction's size and then see which is smaller!
Add this new function, compareTxSize, to your code, and then we will walk through it together:
async function compareTxSize() {
// Step 1 - Fetch the lookup table
const lookupTable = (await SOLANA_CONNECTION.getAddressLookupTable(LOOKUP_TABLE_ADDRESS)).value;
if (!lookupTable) return;
console.log(" ✅ - Fetched lookup table:", lookupTable.key.toString());
// Step 2 - Generate an array of Solana transfer instruction to each address in our lookup table
const txInstructions: TransactionInstruction[] = [];
for (let i = 0; i < lookupTable.state.addresses.length; i++) {
const address = lookupTable.state.addresses[i];
txInstructions.push(
SystemProgram.transfer({
fromPubkey: SIGNER_WALLET.publicKey,
toPubkey: address,
lamports: 0.01 * LAMPORTS_PER_SOL,
})
)
}
// Step 3 - Fetch the latest Blockhash
let latestBlockhash = await SOLANA_CONNECTION.getLatestBlockhash('finalized');
console.log(" ✅ - Fetched latest blockhash. Last valid height:", latestBlockhash.lastValidBlockHeight);
// Step 4 - Generate and sign a transaction that uses a lookup table
const messageWithLookupTable = new TransactionMessage({
payerKey: SIGNER_WALLET.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: txInstructions
}).compileToV0Message([lookupTable]); // 👈 NOTE: We DO include the lookup table
const transactionWithLookupTable = new VersionedTransaction(messageWithLookupTable);
transactionWithLookupTable.sign([SIGNER_WALLET]);
// Step 5 - Generate and sign a transaction that DOES NOT use a lookup table
const messageWithoutLookupTable = new TransactionMessage({
payerKey: SIGNER_WALLET.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: txInstructions
}).compileToV0Message(); // 👈 NOTE: We do NOT include the lookup table
const transactionWithoutLookupTable = new VersionedTransaction(messageWithoutLookupTable);
transactionWithoutLookupTable.sign([SIGNER_WALLET]);
console.log(" ✅ - Compiled transactions");
// Step 6 - Log our transaction size
console.log('Transaction size without address lookup table: ', transactionWithoutLookupTable.serialize().length, 'bytes');
console.log('Transaction size with address lookup table: ', transactionWithLookupTable.serialize().length, 'bytes');
}
This looks like a lot, but it's reusing the code we created earlier in this guide. Let's step through: Step 1. We fetch our lookup table and make sure that we return a valid result. Step 2. Using a similar loop to our previous address log exercise, we iterate through each address in our table, create a transfer instruction to that address, and we push each TransactionInstruction to an array, txInstructions. Step 3. We fetch the latest blockhash, which we need to generate our messages. Step 4. We generate a transaction that uses our lookup table. Note that we pass [lookupTable] into the .compileToV0Message() method. When we used this before, we did not pass a value here. This lets our transaction know to use our lookup table. To calculate the size of our transaction, we also need to sign it--we do so with .sign(). Step 5. We generate the same transaction WITHOUT calling our lookup table. We now have two transactions that should process the same instructions, but one uses a lookup table, and the other just uses the public keys that were passed into each instruction. Step 6. Finally, we log each transaction's size. You can calculate a transaction size by serializing it using .serialize() (which yields a Uint8Array) and calculating its length using .length.
We did a lot there, but I hope everything is starting to feel familiar! Call your function by adding compareTxSize(); to the end of your code:
compareTxSize();
In your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.
Do you see results like this?
Transaction Size without Address Lookup Table: 413 bytes
Transaction Size with Address Lookup Table: 292 bytes
Wow! That's a lot of savings. My lookup table saved me over 120 bytes (your savings may be more or less depending on the number of addresses in your table)! In an environment where transactions are limited to 1,232 bytes, that's a big deal.
總結
That concludes our guide! If you'd like to check your final code against ours, you can find it on GitHub, here.
Address Lookup Tables are an exciting new addition to Solana. As you continue building out more complex transactions, you should find that they create flexibility in how you approach your programs and dApps.
**Want to keep building?
**
- Try adding a UI to your lookup table in your Solana Explorer Clone.
- See how many Solana airdrops you can include in a Bulk Transaction
如果你遇到困難、有任何疑問,或是單純想聊聊你正在開發的專案,歡迎在Discord或Twitter 上聯絡我們!
我們 ❤️ 您的回饋!
如果您對這份指南有任何意見或建議,請告訴我們。我們非常樂意聽取您的意見。
