feat(solana_utils): support account lookup table (#1424)

* feat: support account lookup table

* remove console log

* Support ALT

* Commas

* feat: support lta

* Go

* Bump
This commit is contained in:
guibescos 2024-04-09 13:51:29 +01:00 committed by GitHub
parent bb830e1760
commit 62d189e3b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 22 deletions

2
package-lock.json generated
View File

@ -59582,7 +59582,7 @@
}, },
"target_chains/solana/sdk/js/solana_utils": { "target_chains/solana/sdk/js/solana_utils": {
"name": "@pythnetwork/solana-utils", "name": "@pythnetwork/solana-utils",
"version": "0.2.0", "version": "0.3.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@coral-xyz/anchor": "^0.29.0", "@coral-xyz/anchor": "^0.29.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@pythnetwork/solana-utils", "name": "@pythnetwork/solana-utils",
"version": "0.2.0", "version": "0.3.0",
"description": "Utility functions for Solana", "description": "Utility functions for Solana",
"homepage": "https://pyth.network", "homepage": "https://pyth.network",
"main": "lib/index.js", "main": "lib/index.js",

View File

@ -1,4 +1,5 @@
import { import {
AddressLookupTableAccount,
ComputeBudgetProgram, ComputeBudgetProgram,
Keypair, Keypair,
PublicKey, PublicKey,
@ -81,4 +82,33 @@ it("Unit test for getSizeOfTransaction", async () => {
expect(versionedTransaction.serialize().length).toBe( expect(versionedTransaction.serialize().length).toBe(
getSizeOfTransaction(ixsToSend) getSizeOfTransaction(ixsToSend)
); );
const addressLookupTable: AddressLookupTableAccount =
new AddressLookupTableAccount({
key: PublicKey.unique(),
state: {
lastExtendedSlot: 0,
lastExtendedSlotStartIndex: 0,
deactivationSlot: BigInt(0),
addresses: [
SystemProgram.programId,
ComputeBudgetProgram.programId,
...ixsToSend[0].keys.map((key) => key.pubkey),
...ixsToSend[1].keys.map((key) => key.pubkey),
...ixsToSend[2].keys.map((key) => key.pubkey),
],
},
});
const versionedTransactionWithAlt = new VersionedTransaction(
new TransactionMessage({
recentBlockhash: transaction.recentBlockhash,
payerKey: payer.publicKey,
instructions: ixsToSend,
}).compileToV0Message([addressLookupTable])
);
expect(versionedTransactionWithAlt.serialize().length).toBe(
getSizeOfTransaction(ixsToSend, true, addressLookupTable)
);
}); });

View File

@ -1,5 +1,6 @@
import { Wallet } from "@coral-xyz/anchor"; import { Wallet } from "@coral-xyz/anchor";
import { import {
AddressLookupTableAccount,
ComputeBudgetProgram, ComputeBudgetProgram,
Connection, Connection,
PACKET_DATA_SIZE, PACKET_DATA_SIZE,
@ -63,7 +64,7 @@ export const DEFAULT_PRIORITY_FEE_CONFIG: PriorityFeeConfig = {
* - A compact array of instructions * - A compact array of instructions
* *
* If the transaction is a `VersionedTransaction`, it also contains an extra byte at the beginning, indicating the version and an array of `MessageAddressTableLookup` at the end. * If the transaction is a `VersionedTransaction`, it also contains an extra byte at the beginning, indicating the version and an array of `MessageAddressTableLookup` at the end.
* We don't support Account Lookup Tables, so that array has a size of 0. * After this field there is an array of indexes into the address lookup table that represents the accounts from the address lookup table used in the transaction.
* *
* Each instruction has the following layout : * Each instruction has the following layout :
* - One byte indicating the index of the program in the account addresses array * - One byte indicating the index of the program in the account addresses array
@ -72,19 +73,22 @@ export const DEFAULT_PRIORITY_FEE_CONFIG: PriorityFeeConfig = {
*/ */
export function getSizeOfTransaction( export function getSizeOfTransaction(
instructions: TransactionInstruction[], instructions: TransactionInstruction[],
versionedTransaction = true versionedTransaction = true,
addressLookupTable?: AddressLookupTableAccount
): number { ): number {
const programs = new Set<string>();
const signers = new Set<string>(); const signers = new Set<string>();
const accounts = new Set<string>(); let accounts = new Set<string>();
instructions.map((ix) => { instructions.map((ix) => {
accounts.add(ix.programId.toBase58()), programs.add(ix.programId.toBase58());
ix.keys.map((key) => { accounts.add(ix.programId.toBase58());
if (key.isSigner) { ix.keys.map((key) => {
signers.add(key.pubkey.toBase58()); if (key.isSigner) {
} signers.add(key.pubkey.toBase58());
accounts.add(key.pubkey.toBase58()); }
}); accounts.add(key.pubkey.toBase58());
});
}); });
const instruction_sizes: number = instructions const instruction_sizes: number = instructions
@ -98,6 +102,19 @@ export function getSizeOfTransaction(
) )
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
let numberOfAddressLookups = 0;
if (addressLookupTable) {
const lookupTableAddresses = addressLookupTable.state.addresses.map(
(address) => address.toBase58()
);
const totalNumberOfAccounts = accounts.size;
accounts = new Set(
[...accounts].filter((account) => !lookupTableAddresses.includes(account))
);
accounts = new Set([...accounts, ...programs, ...signers]);
numberOfAddressLookups = totalNumberOfAccounts - accounts.size; // This number is equal to the number of accounts that are in the lookup table and are neither signers nor programs
}
return ( return (
getSizeOfCompressedU16(signers.size) + getSizeOfCompressedU16(signers.size) +
signers.size * 64 + // array of signatures signers.size * 64 + // array of signatures
@ -107,7 +124,10 @@ export function getSizeOfTransaction(
32 + // recent blockhash 32 + // recent blockhash
getSizeOfCompressedU16(instructions.length) + getSizeOfCompressedU16(instructions.length) +
instruction_sizes + // array of instructions instruction_sizes + // array of instructions
(versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) // we don't support Account Lookup Tables (versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) + // transaction version and number of address lookup tables
(versionedTransaction && addressLookupTable ? 32 : 0) + // address lookup table address (we only support 1 address lookup table)
(versionedTransaction && addressLookupTable ? 2 : 0) + // number of address lookup indexes
numberOfAddressLookups // address lookup indexes
); );
} }
@ -130,11 +150,17 @@ export class TransactionBuilder {
}[] = []; }[] = [];
readonly payer: PublicKey; readonly payer: PublicKey;
readonly connection: Connection; readonly connection: Connection;
readonly addressLookupTable: AddressLookupTableAccount | undefined;
/** Make a new `TransactionBuilder`. It requires a `payer` to populate the `payerKey` field and a connection to populate `recentBlockhash` in the versioned transactions. */ /** Make a new `TransactionBuilder`. It requires a `payer` to populate the `payerKey` field and a connection to populate `recentBlockhash` in the versioned transactions. */
constructor(payer: PublicKey, connection: Connection) { constructor(
payer: PublicKey,
connection: Connection,
accountLookupTable?: AddressLookupTableAccount
) {
this.payer = payer; this.payer = payer;
this.connection = connection; this.connection = connection;
this.addressLookupTable = accountLookupTable;
} }
/** /**
@ -149,11 +175,16 @@ export class TransactionBuilder {
computeUnits: computeUnits ?? 0, computeUnits: computeUnits ?? 0,
}); });
} else if ( } else if (
getSizeOfTransaction([ getSizeOfTransaction(
...this.transactionInstructions[this.transactionInstructions.length - 1] [
.instructions, ...this.transactionInstructions[
instruction, this.transactionInstructions.length - 1
]) <= PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET ].instructions,
instruction,
],
true,
this.addressLookupTable
) <= PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET
) { ) {
this.transactionInstructions[ this.transactionInstructions[
this.transactionInstructions.length - 1 this.transactionInstructions.length - 1
@ -218,7 +249,9 @@ export class TransactionBuilder {
recentBlockhash: blockhash, recentBlockhash: blockhash,
instructions: instructionsWithComputeBudget, instructions: instructionsWithComputeBudget,
payerKey: this.payer, payerKey: this.payer,
}).compileToV0Message() }).compileToV0Message(
this.addressLookupTable ? [this.addressLookupTable] : []
)
), ),
signers: signers, signers: signers,
}; };
@ -289,9 +322,14 @@ export class TransactionBuilder {
payer: PublicKey, payer: PublicKey,
connection: Connection, connection: Connection,
instructions: InstructionWithEphemeralSigners[], instructions: InstructionWithEphemeralSigners[],
priorityFeeConfig: PriorityFeeConfig priorityFeeConfig: PriorityFeeConfig,
addressLookupTable?: AddressLookupTableAccount
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> { ): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
const transactionBuilder = new TransactionBuilder(payer, connection); const transactionBuilder = new TransactionBuilder(
payer,
connection,
addressLookupTable
);
transactionBuilder.addInstructions(instructions); transactionBuilder.addInstructions(instructions);
return transactionBuilder.buildVersionedTransactions(priorityFeeConfig); return transactionBuilder.buildVersionedTransactions(priorityFeeConfig);
} }