From 62d189e3b5e3cdbbfba04c1ebd677b28f4a6f693 Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:51:29 +0100 Subject: [PATCH] feat(solana_utils): support account lookup table (#1424) * feat: support account lookup table * remove console log * Support ALT * Commas * feat: support lta * Go * Bump --- package-lock.json | 2 +- .../solana/sdk/js/solana_utils/package.json | 2 +- .../src/__tests__/TransactionSize.test.ts | 30 +++++++ .../sdk/js/solana_utils/src/transaction.ts | 78 ++++++++++++++----- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index e026f79d..e8fa3fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59582,7 +59582,7 @@ }, "target_chains/solana/sdk/js/solana_utils": { "name": "@pythnetwork/solana-utils", - "version": "0.2.0", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.29.0", diff --git a/target_chains/solana/sdk/js/solana_utils/package.json b/target_chains/solana/sdk/js/solana_utils/package.json index 305deab0..e0fac496 100644 --- a/target_chains/solana/sdk/js/solana_utils/package.json +++ b/target_chains/solana/sdk/js/solana_utils/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/solana-utils", - "version": "0.2.0", + "version": "0.3.0", "description": "Utility functions for Solana", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/target_chains/solana/sdk/js/solana_utils/src/__tests__/TransactionSize.test.ts b/target_chains/solana/sdk/js/solana_utils/src/__tests__/TransactionSize.test.ts index 1215ae8d..305e80f1 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/__tests__/TransactionSize.test.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/__tests__/TransactionSize.test.ts @@ -1,4 +1,5 @@ import { + AddressLookupTableAccount, ComputeBudgetProgram, Keypair, PublicKey, @@ -81,4 +82,33 @@ it("Unit test for getSizeOfTransaction", async () => { expect(versionedTransaction.serialize().length).toBe( 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) + ); }); diff --git a/target_chains/solana/sdk/js/solana_utils/src/transaction.ts b/target_chains/solana/sdk/js/solana_utils/src/transaction.ts index cfbb0147..fecb0c53 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/transaction.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/transaction.ts @@ -1,5 +1,6 @@ import { Wallet } from "@coral-xyz/anchor"; import { + AddressLookupTableAccount, ComputeBudgetProgram, Connection, PACKET_DATA_SIZE, @@ -63,7 +64,7 @@ export const DEFAULT_PRIORITY_FEE_CONFIG: PriorityFeeConfig = { * - 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. - * 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 : * - 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( instructions: TransactionInstruction[], - versionedTransaction = true + versionedTransaction = true, + addressLookupTable?: AddressLookupTableAccount ): number { + const programs = new Set(); const signers = new Set(); - const accounts = new Set(); + let accounts = new Set(); instructions.map((ix) => { - accounts.add(ix.programId.toBase58()), - ix.keys.map((key) => { - if (key.isSigner) { - signers.add(key.pubkey.toBase58()); - } - accounts.add(key.pubkey.toBase58()); - }); + programs.add(ix.programId.toBase58()); + accounts.add(ix.programId.toBase58()); + ix.keys.map((key) => { + if (key.isSigner) { + signers.add(key.pubkey.toBase58()); + } + accounts.add(key.pubkey.toBase58()); + }); }); const instruction_sizes: number = instructions @@ -98,6 +102,19 @@ export function getSizeOfTransaction( ) .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 ( getSizeOfCompressedU16(signers.size) + signers.size * 64 + // array of signatures @@ -107,7 +124,10 @@ export function getSizeOfTransaction( 32 + // recent blockhash getSizeOfCompressedU16(instructions.length) + 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 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. */ - constructor(payer: PublicKey, connection: Connection) { + constructor( + payer: PublicKey, + connection: Connection, + accountLookupTable?: AddressLookupTableAccount + ) { this.payer = payer; this.connection = connection; + this.addressLookupTable = accountLookupTable; } /** @@ -149,11 +175,16 @@ export class TransactionBuilder { computeUnits: computeUnits ?? 0, }); } else if ( - getSizeOfTransaction([ - ...this.transactionInstructions[this.transactionInstructions.length - 1] - .instructions, - instruction, - ]) <= PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET + getSizeOfTransaction( + [ + ...this.transactionInstructions[ + this.transactionInstructions.length - 1 + ].instructions, + instruction, + ], + true, + this.addressLookupTable + ) <= PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET ) { this.transactionInstructions[ this.transactionInstructions.length - 1 @@ -218,7 +249,9 @@ export class TransactionBuilder { recentBlockhash: blockhash, instructions: instructionsWithComputeBudget, payerKey: this.payer, - }).compileToV0Message() + }).compileToV0Message( + this.addressLookupTable ? [this.addressLookupTable] : [] + ) ), signers: signers, }; @@ -289,9 +322,14 @@ export class TransactionBuilder { payer: PublicKey, connection: Connection, instructions: InstructionWithEphemeralSigners[], - priorityFeeConfig: PriorityFeeConfig + priorityFeeConfig: PriorityFeeConfig, + addressLookupTable?: AddressLookupTableAccount ): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> { - const transactionBuilder = new TransactionBuilder(payer, connection); + const transactionBuilder = new TransactionBuilder( + payer, + connection, + addressLookupTable + ); transactionBuilder.addInstructions(instructions); return transactionBuilder.buildVersionedTransactions(priorityFeeConfig); }