Skip to content

Commit

Permalink
feat: add getAddressLookupTable method to Connection (#27127)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry authored Aug 14, 2022
1 parent 0ca8239 commit dcef8ec
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 7 deletions.
39 changes: 39 additions & 0 deletions web3.js/src/account-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as BufferLayout from '@solana/buffer-layout';

export interface IAccountStateData {
readonly typeIndex: number;
}

/**
* @internal
*/
export type AccountType<TInputData extends IAccountStateData> = {
/** The account type index (from solana upstream program) */
index: number;
/** The BufferLayout to use to build data */
layout: BufferLayout.Layout<TInputData>;
};

/**
* Decode account data buffer using an AccountType
* @internal
*/
export function decodeData<TAccountStateData extends IAccountStateData>(
type: AccountType<TAccountStateData>,
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;
}
26 changes: 25 additions & 1 deletion web3.js/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -4218,6 +4219,29 @@ export class Connection {
return res.result;
}

async getAddressLookupTable(
accountKey: PublicKey,
config?: GetAccountInfoConfig,
): Promise<RpcResponseAndContext<AddressLookupTableAccount | null>> {
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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down
84 changes: 84 additions & 0 deletions web3.js/src/programs/address-lookup-table/state.ts
Original file line number Diff line number Diff line change
@@ -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<PublicKey>;
};

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<Uint8Array>}>([
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<Uint8Array>;
}>([
BufferLayout.u32('typeIndex'),
u64('deactivationSlot'),
BufferLayout.nu64('lastExtendedSlot'),
BufferLayout.u8('lastExtendedStartIndex'),
BufferLayout.u8(), // option
BufferLayout.seq(
Layout.publicKey(),
BufferLayout.offset(BufferLayout.u8(), -1),
'authority',
),
]),
};
86 changes: 86 additions & 0 deletions web3.js/test/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
sendAndConfirmTransaction,
Keypair,
Message,
AddressLookupTableProgram,
} from '../src';
import invariant from '../src/utils/assert';
import {MOCK_PORT, url} from './url';
Expand Down Expand Up @@ -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;
}
});
}
});

0 comments on commit dcef8ec

Please sign in to comment.