From dcef8ec1006596ed1f46594b16b147e5e809395e Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 14 Aug 2022 11:11:49 +0100 Subject: [PATCH] feat: add getAddressLookupTable method to Connection (#27127) --- web3.js/src/account-data.ts | 39 +++++++++ web3.js/src/connection.ts | 26 +++++- .../index.ts} | 14 +-- .../programs/address-lookup-table/state.ts | 84 ++++++++++++++++++ web3.js/test/connection.test.ts | 86 +++++++++++++++++++ 5 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 web3.js/src/account-data.ts rename web3.js/src/programs/{address-lookup-table.ts => address-lookup-table/index.ts} (97%) create mode 100644 web3.js/src/programs/address-lookup-table/state.ts diff --git a/web3.js/src/account-data.ts b/web3.js/src/account-data.ts new file mode 100644 index 0000000000..a61d695c64 --- /dev/null +++ b/web3.js/src/account-data.ts @@ -0,0 +1,39 @@ +import * as BufferLayout from '@solana/buffer-layout'; + +export interface IAccountStateData { + readonly typeIndex: number; +} + +/** + * @internal + */ +export type AccountType = { + /** The account type index (from solana upstream program) */ + index: number; + /** The BufferLayout to use to build data */ + layout: BufferLayout.Layout; +}; + +/** + * Decode account data buffer using an AccountType + * @internal + */ +export function decodeData( + type: AccountType, + data: Uint8Array, +): TAccountStateData { + let decoded: TAccountStateData; + try { + decoded = type.layout.decode(data); + } catch (err) { + throw new Error('invalid instruction; ' + err); + } + + if (decoded.typeIndex !== type.index) { + throw new Error( + `invalid account data; account type mismatch ${decoded.typeIndex} != ${type.index}`, + ); + } + + return decoded; +} diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 6a5db84406..0fb2e5a9b1 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -24,7 +24,6 @@ import type {Struct} from 'superstruct'; import {Client as RpcWebSocketClient} from 'rpc-websockets'; import RpcClient from 'jayson/lib/client/browser'; -import {URL} from './utils/url-impl'; import {AgentManager} from './agent-manager'; import {EpochSchedule} from './epoch-schedule'; import {SendTransactionError, SolanaJSONRPCError} from './errors'; @@ -35,6 +34,7 @@ import {Signer} from './keypair'; import {MS_PER_SLOT} from './timing'; import {Transaction, TransactionStatus} from './transaction'; import {Message} from './message'; +import {AddressLookupTableAccount} from './programs/address-lookup-table/state'; import assert from './utils/assert'; import {sleep} from './utils/sleep'; import {toBuffer} from './utils/to-buffer'; @@ -43,6 +43,7 @@ import { TransactionExpiredTimeoutError, } from './transaction/expiry-custom-errors'; import {makeWebsocketUrl} from './utils/makeWebsocketUrl'; +import {URL} from './utils/url-impl'; import type {Blockhash} from './blockhash'; import type {FeeCalculator} from './fee-calculator'; import type {TransactionSignature} from './transaction'; @@ -4218,6 +4219,29 @@ export class Connection { return res.result; } + async getAddressLookupTable( + accountKey: PublicKey, + config?: GetAccountInfoConfig, + ): Promise> { + const {context, value: accountInfo} = await this.getAccountInfoAndContext( + accountKey, + config, + ); + + let value = null; + if (accountInfo !== null) { + value = new AddressLookupTableAccount({ + key: accountKey, + state: AddressLookupTableAccount.deserialize(accountInfo.data), + }); + } + + return { + context, + value, + }; + } + /** * Fetch the contents of a Nonce account from the cluster, return with context */ diff --git a/web3.js/src/programs/address-lookup-table.ts b/web3.js/src/programs/address-lookup-table/index.ts similarity index 97% rename from web3.js/src/programs/address-lookup-table.ts rename to web3.js/src/programs/address-lookup-table/index.ts index d367d4ea70..da752ddb4f 100644 --- a/web3.js/src/programs/address-lookup-table.ts +++ b/web3.js/src/programs/address-lookup-table/index.ts @@ -1,12 +1,14 @@ import {toBufferLE} from 'bigint-buffer'; import * as BufferLayout from '@solana/buffer-layout'; -import * as Layout from '../layout'; -import {PublicKey} from '../publickey'; -import * as bigintLayout from '../utils/bigint'; -import {SystemProgram} from './system'; -import {TransactionInstruction} from '../transaction'; -import {decodeData, encodeData, IInstructionInputData} from '../instruction'; +import * as Layout from '../../layout'; +import {PublicKey} from '../../publickey'; +import * as bigintLayout from '../../utils/bigint'; +import {SystemProgram} from '../system'; +import {TransactionInstruction} from '../../transaction'; +import {decodeData, encodeData, IInstructionInputData} from '../../instruction'; + +export * from './state'; export type CreateLookupTableParams = { /** Account used to derive and control the new address lookup table. */ diff --git a/web3.js/src/programs/address-lookup-table/state.ts b/web3.js/src/programs/address-lookup-table/state.ts new file mode 100644 index 0000000000..6f4432b25f --- /dev/null +++ b/web3.js/src/programs/address-lookup-table/state.ts @@ -0,0 +1,84 @@ +import * as BufferLayout from '@solana/buffer-layout'; + +import assert from '../../utils/assert'; +import * as Layout from '../../layout'; +import {PublicKey} from '../../publickey'; +import {u64} from '../../utils/bigint'; +import {decodeData} from '../../account-data'; + +export type AddressLookupTableState = { + deactivationSlot: bigint; + lastExtendedSlot: number; + lastExtendedSlotStartIndex: number; + authority?: PublicKey; + addresses: Array; +}; + +export type AddressLookupTableAccountArgs = { + key: PublicKey; + state: AddressLookupTableState; +}; + +/// The serialized size of lookup table metadata +const LOOKUP_TABLE_META_SIZE = 56; + +export class AddressLookupTableAccount { + key: PublicKey; + state: AddressLookupTableState; + + constructor(args: AddressLookupTableAccountArgs) { + this.key = args.key; + this.state = args.state; + } + + isActive(): boolean { + const U64_MAX = 2n ** 64n - 1n; + return this.state.deactivationSlot === U64_MAX; + } + + static deserialize(accountData: Uint8Array): AddressLookupTableState { + const meta = decodeData(LookupTableMetaLayout, accountData); + + const serializedAddressesLen = accountData.length - LOOKUP_TABLE_META_SIZE; + assert(serializedAddressesLen >= 0, 'lookup table is invalid'); + assert(serializedAddressesLen % 32 === 0, 'lookup table is invalid'); + + const numSerializedAddresses = serializedAddressesLen / 32; + const {addresses} = BufferLayout.struct<{addresses: Array}>([ + BufferLayout.seq(Layout.publicKey(), numSerializedAddresses, 'addresses'), + ]).decode(accountData.slice(LOOKUP_TABLE_META_SIZE)); + + return { + deactivationSlot: meta.deactivationSlot, + lastExtendedSlot: meta.lastExtendedSlot, + lastExtendedSlotStartIndex: meta.lastExtendedStartIndex, + authority: + meta.authority.length !== 0 + ? new PublicKey(meta.authority[0]) + : undefined, + addresses: addresses.map(address => new PublicKey(address)), + }; + } +} + +const LookupTableMetaLayout = { + index: 1, + layout: BufferLayout.struct<{ + typeIndex: number; + deactivationSlot: bigint; + lastExtendedSlot: number; + lastExtendedStartIndex: number; + authority: Array; + }>([ + BufferLayout.u32('typeIndex'), + u64('deactivationSlot'), + BufferLayout.nu64('lastExtendedSlot'), + BufferLayout.u8('lastExtendedStartIndex'), + BufferLayout.u8(), // option + BufferLayout.seq( + Layout.publicKey(), + BufferLayout.offset(BufferLayout.u8(), -1), + 'authority', + ), + ]), +}; diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 8149737aec..10b62f04dd 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -18,6 +18,7 @@ import { sendAndConfirmTransaction, Keypair, Message, + AddressLookupTableProgram, } from '../src'; import invariant from '../src/utils/assert'; import {MOCK_PORT, url} from './url'; @@ -4243,5 +4244,90 @@ describe('Connection', function () { const version = await connection.getVersion(); expect(version['solana-core']).to.be.ok; }).timeout(20 * 1000); + + it('getAddressLookupTable', async () => { + const payer = Keypair.generate(); + + await helpers.airdrop({ + connection, + address: payer.publicKey, + amount: LAMPORTS_PER_SOL, + }); + + const lookupTableAddresses = new Array(10) + .fill(0) + .map(() => Keypair.generate().publicKey); + + const recentSlot = await connection.getSlot('finalized'); + const [createIx, lookupTableKey] = + AddressLookupTableProgram.createLookupTable({ + recentSlot, + payer: payer.publicKey, + authority: payer.publicKey, + }); + + // create, extend, and fetch + { + const transaction = new Transaction().add(createIx).add( + AddressLookupTableProgram.extendLookupTable({ + lookupTable: lookupTableKey, + addresses: lookupTableAddresses, + authority: payer.publicKey, + payer: payer.publicKey, + }), + ); + await helpers.processTransaction({ + connection, + transaction, + signers: [payer], + commitment: 'processed', + }); + + const lookupTableResponse = await connection.getAddressLookupTable( + lookupTableKey, + { + commitment: 'processed', + }, + ); + const lookupTableAccount = lookupTableResponse.value; + if (!lookupTableAccount) { + expect(lookupTableAccount).to.be.ok; + return; + } + expect(lookupTableAccount.isActive()).to.be.true; + expect(lookupTableAccount.state.authority).to.eql(payer.publicKey); + expect(lookupTableAccount.state.addresses).to.eql(lookupTableAddresses); + } + + // freeze and fetch + { + const transaction = new Transaction().add( + AddressLookupTableProgram.freezeLookupTable({ + lookupTable: lookupTableKey, + authority: payer.publicKey, + }), + ); + await helpers.processTransaction({ + connection, + transaction, + signers: [payer], + commitment: 'processed', + }); + + const lookupTableResponse = await connection.getAddressLookupTable( + lookupTableKey, + { + commitment: 'processed', + }, + ); + const lookupTableAccount = lookupTableResponse.value; + if (!lookupTableAccount) { + expect(lookupTableAccount).to.be.ok; + return; + } + expect(lookupTableAccount.isActive()).to.be.true; + expect(lookupTableAccount.state.authority).to.be.undefined; + } + }); } });