feat: added web3 bindings for Address Lookup Table Program instructions (#26469)

* feat: added web3 bindings for Address Lookup Table Program

* fix: refactoring + addresses PR comments

* fix: typos fixed and minor refactoring

* add lookup table instruction decoding support + fixes recent slot serialization bug

* export lookup table program

* linting

* fix: type annotations

* add tests cases for address lookup table program

* fix: alloc encoding buffer properly for seq layouts

* fix: typedoc issue

Co-authored-by: Antematter <hello@antematter.io>
Co-authored-by: Muhammad Saad <msaadahmed039@gmail.com>
Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
Antematter 2022-07-14 00:19:51 +05:00 committed by GitHub
parent 048b9f670b
commit e3c9c58032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 717 additions and 5 deletions

View File

@ -0,0 +1,433 @@
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 './util/bigint';
import {SystemProgram} from './system-program';
import {TransactionInstruction} from './transaction';
import {decodeData, encodeData, IInstructionInputData} from './instruction';
export type CreateLookupTableParams = {
/** Account used to derive and control the new address lookup table. */
authority: PublicKey;
/** Account that will fund the new address lookup table. */
payer: PublicKey;
/** A recent slot must be used in the derivation path for each initialized table. */
recentSlot: bigint | number;
};
export type FreezeLookupTableParams = {
/** Address lookup table account to freeze. */
lookupTable: PublicKey;
/** Account which is the current authority. */
authority: PublicKey;
};
export type ExtendLookupTableParams = {
/** Address lookup table account to extend. */
lookupTable: PublicKey;
/** Account which is the current authority. */
authority: PublicKey;
/** Account that will fund the table reallocation.
* Not required if the reallocation has already been funded. */
payer?: PublicKey;
/** List of Public Keys to be added to the lookup table. */
addresses: Array<PublicKey>;
};
export type DeactivateLookupTableParams = {
/** Address lookup table account to deactivate. */
lookupTable: PublicKey;
/** Account which is the current authority. */
authority: PublicKey;
};
export type CloseLookupTableParams = {
/** Address lookup table account to close. */
lookupTable: PublicKey;
/** Account which is the current authority. */
authority: PublicKey;
/** Recipient of closed account lamports. */
recipient: PublicKey;
};
/**
* An enumeration of valid LookupTableInstructionType's
*/
export type LookupTableInstructionType =
| 'CreateLookupTable'
| 'ExtendLookupTable'
| 'CloseLookupTable'
| 'FreezeLookupTable'
| 'DeactivateLookupTable';
type LookupTableInstructionInputData = {
CreateLookupTable: IInstructionInputData &
Readonly<{
recentSlot: bigint;
bumpSeed: number;
}>;
FreezeLookupTable: IInstructionInputData;
ExtendLookupTable: IInstructionInputData &
Readonly<{
numberOfAddresses: bigint;
addresses: Array<Uint8Array>;
}>;
DeactivateLookupTable: IInstructionInputData;
CloseLookupTable: IInstructionInputData;
};
/**
* An enumeration of valid address lookup table InstructionType's
* @internal
*/
export const LOOKUP_TABLE_INSTRUCTION_LAYOUTS = Object.freeze({
CreateLookupTable: {
index: 0,
layout: BufferLayout.struct<
LookupTableInstructionInputData['CreateLookupTable']
>([
BufferLayout.u32('instruction'),
bigintLayout.u64('recentSlot'),
BufferLayout.u8('bumpSeed'),
]),
},
FreezeLookupTable: {
index: 1,
layout: BufferLayout.struct<
LookupTableInstructionInputData['FreezeLookupTable']
>([BufferLayout.u32('instruction')]),
},
ExtendLookupTable: {
index: 2,
layout: BufferLayout.struct<
LookupTableInstructionInputData['ExtendLookupTable']
>([
BufferLayout.u32('instruction'),
bigintLayout.u64(),
BufferLayout.seq(
Layout.publicKey(),
BufferLayout.offset(BufferLayout.u32(), -8),
'addresses',
),
]),
},
DeactivateLookupTable: {
index: 3,
layout: BufferLayout.struct<
LookupTableInstructionInputData['DeactivateLookupTable']
>([BufferLayout.u32('instruction')]),
},
CloseLookupTable: {
index: 4,
layout: BufferLayout.struct<
LookupTableInstructionInputData['CloseLookupTable']
>([BufferLayout.u32('instruction')]),
},
});
export class AddressLookupTableInstruction {
/**
* @internal
*/
constructor() {}
static decodeInstructionType(
instruction: TransactionInstruction,
): LookupTableInstructionType {
this.checkProgramId(instruction.programId);
const instructionTypeLayout = BufferLayout.u32('instruction');
const index = instructionTypeLayout.decode(instruction.data);
let type: LookupTableInstructionType | undefined;
for (const [layoutType, layout] of Object.entries(
LOOKUP_TABLE_INSTRUCTION_LAYOUTS,
)) {
if ((layout as any).index == index) {
type = layoutType as LookupTableInstructionType;
break;
}
}
if (!type) {
throw new Error(
'Invalid Instruction. Should be a LookupTable Instruction',
);
}
return type;
}
static decodeCreateLookupTable(
instruction: TransactionInstruction,
): CreateLookupTableParams {
this.checkProgramId(instruction.programId);
this.checkKeysLength(instruction.keys, 4);
const {recentSlot} = decodeData(
LOOKUP_TABLE_INSTRUCTION_LAYOUTS.CreateLookupTable,
instruction.data,
);
return {
authority: instruction.keys[1].pubkey,
payer: instruction.keys[2].pubkey,
recentSlot: Number(recentSlot),
};
}
static decodeExtendLookupTable(
instruction: TransactionInstruction,
): ExtendLookupTableParams {
this.checkProgramId(instruction.programId);
if (instruction.keys.length < 2) {
throw new Error(
`invalid instruction; found ${instruction.keys.length} keys, expected at least 2`,
);
}
const {addresses} = decodeData(
LOOKUP_TABLE_INSTRUCTION_LAYOUTS.ExtendLookupTable,
instruction.data,
);
return {
lookupTable: instruction.keys[0].pubkey,
authority: instruction.keys[1].pubkey,
payer:
instruction.keys.length > 2 ? instruction.keys[2].pubkey : undefined,
addresses: addresses.map(buffer => new PublicKey(buffer)),
};
}
static decodeCloseLookupTable(
instruction: TransactionInstruction,
): CloseLookupTableParams {
this.checkProgramId(instruction.programId);
this.checkKeysLength(instruction.keys, 3);
return {
lookupTable: instruction.keys[0].pubkey,
authority: instruction.keys[1].pubkey,
recipient: instruction.keys[2].pubkey,
};
}
static decodeFreezeLookupTable(
instruction: TransactionInstruction,
): FreezeLookupTableParams {
this.checkProgramId(instruction.programId);
this.checkKeysLength(instruction.keys, 2);
return {
lookupTable: instruction.keys[0].pubkey,
authority: instruction.keys[1].pubkey,
};
}
static decodeDeactivateLookupTable(
instruction: TransactionInstruction,
): DeactivateLookupTableParams {
this.checkProgramId(instruction.programId);
this.checkKeysLength(instruction.keys, 2);
return {
lookupTable: instruction.keys[0].pubkey,
authority: instruction.keys[1].pubkey,
};
}
/**
* @internal
*/
static checkProgramId(programId: PublicKey) {
if (!programId.equals(AddressLookupTableProgram.programId)) {
throw new Error(
'invalid instruction; programId is not AddressLookupTable Program',
);
}
}
/**
* @internal
*/
static checkKeysLength(keys: Array<any>, expectedLength: number) {
if (keys.length < expectedLength) {
throw new Error(
`invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`,
);
}
}
}
export class AddressLookupTableProgram {
/**
* @internal
*/
constructor() {}
static programId: PublicKey = new PublicKey(
'AddressLookupTab1e1111111111111111111111111',
);
static createLookupTable(params: CreateLookupTableParams) {
const [lookupTableAddress, bumpSeed] = PublicKey.findProgramAddressSync(
[params.authority.toBuffer(), toBufferLE(BigInt(params.recentSlot), 8)],
this.programId,
);
const type = LOOKUP_TABLE_INSTRUCTION_LAYOUTS.CreateLookupTable;
const data = encodeData(type, {
recentSlot: BigInt(params.recentSlot),
bumpSeed: bumpSeed,
});
const keys = [
{
pubkey: lookupTableAddress,
isSigner: false,
isWritable: true,
},
{
pubkey: params.authority,
isSigner: true,
isWritable: false,
},
{
pubkey: params.payer,
isSigner: true,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
];
return [
new TransactionInstruction({
programId: this.programId,
keys: keys,
data: data,
}),
lookupTableAddress,
] as [TransactionInstruction, PublicKey];
}
static freezeLookupTable(params: FreezeLookupTableParams) {
const type = LOOKUP_TABLE_INSTRUCTION_LAYOUTS.FreezeLookupTable;
const data = encodeData(type);
const keys = [
{
pubkey: params.lookupTable,
isSigner: false,
isWritable: true,
},
{
pubkey: params.authority,
isSigner: true,
isWritable: false,
},
];
return new TransactionInstruction({
programId: this.programId,
keys: keys,
data: data,
});
}
static extendLookupTable(params: ExtendLookupTableParams) {
const type = LOOKUP_TABLE_INSTRUCTION_LAYOUTS.ExtendLookupTable;
const data = encodeData(type, {
addresses: params.addresses.map(addr => addr.toBytes()),
});
const keys = [
{
pubkey: params.lookupTable,
isSigner: false,
isWritable: true,
},
{
pubkey: params.authority,
isSigner: true,
isWritable: false,
},
];
if (params.payer) {
keys.push(
{
pubkey: params.payer,
isSigner: true,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
);
}
return new TransactionInstruction({
programId: this.programId,
keys: keys,
data: data,
});
}
static deactivateLookupTable(params: DeactivateLookupTableParams) {
const type = LOOKUP_TABLE_INSTRUCTION_LAYOUTS.DeactivateLookupTable;
const data = encodeData(type);
const keys = [
{
pubkey: params.lookupTable,
isSigner: false,
isWritable: true,
},
{
pubkey: params.authority,
isSigner: true,
isWritable: false,
},
];
return new TransactionInstruction({
programId: this.programId,
keys: keys,
data: data,
});
}
static closeLookupTable(params: CloseLookupTableParams) {
const type = LOOKUP_TABLE_INSTRUCTION_LAYOUTS.CloseLookupTable;
const data = encodeData(type);
const keys = [
{
pubkey: params.lookupTable,
isSigner: false,
isWritable: true,
},
{
pubkey: params.authority,
isSigner: true,
isWritable: false,
},
{
pubkey: params.recipient,
isSigner: false,
isWritable: true,
},
];
return new TransactionInstruction({
programId: this.programId,
keys: keys,
data: data,
});
}
}

View File

@ -1,4 +1,5 @@
export * from './account';
export * from './address-lookup-table-program';
export * from './blockhash';
export * from './bpf-loader-deprecated';
export * from './bpf-loader';

View File

@ -135,13 +135,25 @@ export const voteInit = (property: string = 'voteInit') => {
};
export function getAlloc(type: any, fields: any): number {
const getItemAlloc = (item: any): number => {
if (item.span >= 0) {
return item.span;
} else if (typeof item.alloc === 'function') {
return item.alloc(fields[item.property]);
} else if ('count' in item && 'elementLayout' in item) {
const field = fields[item.property];
if (Array.isArray(field)) {
return field.length * getItemAlloc(item.elementLayout);
}
}
// Couldn't determine allocated size of layout
return 0;
};
let alloc = 0;
type.layout.fields.forEach((item: any) => {
if (item.span >= 0) {
alloc += item.span;
} else if (typeof item.alloc === 'function') {
alloc += item.alloc(fields[item.property]);
}
alloc += getItemAlloc(item);
});
return alloc;
}

View File

@ -0,0 +1,266 @@
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {
Keypair,
AddressLookupTableProgram,
Transaction,
AddressLookupTableInstruction,
Connection,
sendAndConfirmTransaction,
} from '../src';
import {sleep} from '../src/util/sleep';
import {helpers} from './mocks/rpc-http';
import {url} from './url';
use(chaiAsPromised);
describe('AddressLookupTableProgram', () => {
it('createAddressLookupTable', () => {
const recentSlot = 0;
const authorityPubkey = Keypair.generate().publicKey;
const payerPubkey = Keypair.generate().publicKey;
const [instruction] = AddressLookupTableProgram.createLookupTable({
authority: authorityPubkey,
payer: payerPubkey,
recentSlot,
});
const transaction = new Transaction().add(instruction);
const createLutParams = {
authority: authorityPubkey,
payer: payerPubkey,
recentSlot,
};
expect(transaction.instructions).to.have.length(1);
expect(createLutParams).to.eql(
AddressLookupTableInstruction.decodeCreateLookupTable(instruction),
);
});
it('extendLookupTableWithPayer', () => {
const lutAddress = Keypair.generate().publicKey;
const authorityPubkey = Keypair.generate().publicKey;
const payerPubkey = Keypair.generate().publicKey;
const addressesToAdd = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const instruction = AddressLookupTableProgram.extendLookupTable({
lookupTable: lutAddress,
authority: authorityPubkey,
payer: payerPubkey,
addresses: addressesToAdd,
});
const transaction = new Transaction().add(instruction);
const extendLutParams = {
lookupTable: lutAddress,
authority: authorityPubkey,
payer: payerPubkey,
addresses: addressesToAdd,
};
expect(transaction.instructions).to.have.length(1);
expect(extendLutParams).to.eql(
AddressLookupTableInstruction.decodeExtendLookupTable(instruction),
);
});
it('extendLookupTableWithoutPayer', () => {
const lutAddress = Keypair.generate().publicKey;
const authorityPubkey = Keypair.generate().publicKey;
const addressesToAdd = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const instruction = AddressLookupTableProgram.extendLookupTable({
lookupTable: lutAddress,
authority: authorityPubkey,
addresses: addressesToAdd,
});
const transaction = new Transaction().add(instruction);
const extendLutParams = {
lookupTable: lutAddress,
authority: authorityPubkey,
payer: undefined,
addresses: addressesToAdd,
};
expect(transaction.instructions).to.have.length(1);
expect(extendLutParams).to.eql(
AddressLookupTableInstruction.decodeExtendLookupTable(instruction),
);
});
it('closeLookupTable', () => {
const lutAddress = Keypair.generate().publicKey;
const authorityPubkey = Keypair.generate().publicKey;
const recipientPubkey = Keypair.generate().publicKey;
const instruction = AddressLookupTableProgram.closeLookupTable({
lookupTable: lutAddress,
authority: authorityPubkey,
recipient: recipientPubkey,
});
const transaction = new Transaction().add(instruction);
const closeLutParams = {
lookupTable: lutAddress,
authority: authorityPubkey,
recipient: recipientPubkey,
};
expect(transaction.instructions).to.have.length(1);
expect(closeLutParams).to.eql(
AddressLookupTableInstruction.decodeCloseLookupTable(instruction),
);
});
it('freezeLookupTable', () => {
const lutAddress = Keypair.generate().publicKey;
const authorityPubkey = Keypair.generate().publicKey;
const instruction = AddressLookupTableProgram.freezeLookupTable({
lookupTable: lutAddress,
authority: authorityPubkey,
});
const transaction = new Transaction().add(instruction);
const freezeLutParams = {
lookupTable: lutAddress,
authority: authorityPubkey,
};
expect(transaction.instructions).to.have.length(1);
expect(freezeLutParams).to.eql(
AddressLookupTableInstruction.decodeFreezeLookupTable(instruction),
);
});
it('deactivateLookupTable', () => {
const lutAddress = Keypair.generate().publicKey;
const authorityPubkey = Keypair.generate().publicKey;
const instruction = AddressLookupTableProgram.deactivateLookupTable({
lookupTable: lutAddress,
authority: authorityPubkey,
});
const transaction = new Transaction().add(instruction);
const deactivateLutParams = {
lookupTable: lutAddress,
authority: authorityPubkey,
};
expect(transaction.instructions).to.have.length(1);
expect(deactivateLutParams).to.eql(
AddressLookupTableInstruction.decodeDeactivateLookupTable(instruction),
);
});
if (process.env.TEST_LIVE) {
it('live address lookup table actions', async () => {
const connection = new Connection(url, 'confirmed');
const authority = Keypair.generate();
const payer = Keypair.generate();
const slot = await connection.getSlot('confirmed');
const payerMinBalance =
await connection.getMinimumBalanceForRentExemption(44 * 10);
const [createInstruction, lutAddress] =
AddressLookupTableProgram.createLookupTable({
authority: authority.publicKey,
payer: payer.publicKey,
recentSlot: slot,
});
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: payerMinBalance,
});
await helpers.airdrop({
connection,
address: authority.publicKey,
amount: payerMinBalance,
});
// Creating a new lut
const createLutTransaction = new Transaction();
createLutTransaction.add(createInstruction);
createLutTransaction.feePayer = payer.publicKey;
await sendAndConfirmTransaction(
connection,
createLutTransaction,
[authority, payer],
{preflightCommitment: 'confirmed'},
);
await sleep(500);
// Extending a lut without a payer
await helpers.airdrop({
connection,
address: lutAddress,
amount: payerMinBalance,
});
const extendWithoutPayerInstruction =
AddressLookupTableProgram.extendLookupTable({
lookupTable: lutAddress,
authority: authority.publicKey,
addresses: [...Array(10)].map(() => Keypair.generate().publicKey),
});
const extendLutWithoutPayerTransaction = new Transaction();
extendLutWithoutPayerTransaction.add(extendWithoutPayerInstruction);
await sendAndConfirmTransaction(
connection,
extendLutWithoutPayerTransaction,
[authority],
{preflightCommitment: 'confirmed'},
);
// Extending an lut with a payer
const extendWithPayerInstruction =
AddressLookupTableProgram.extendLookupTable({
lookupTable: lutAddress,
authority: authority.publicKey,
payer: payer.publicKey,
addresses: [...Array(10)].map(() => Keypair.generate().publicKey),
});
const extendLutWithPayerTransaction = new Transaction();
extendLutWithPayerTransaction.add(extendWithPayerInstruction);
await sendAndConfirmTransaction(
connection,
extendLutWithPayerTransaction,
[authority, payer],
{preflightCommitment: 'confirmed'},
);
//deactivating the lut
const deactivateInstruction =
AddressLookupTableProgram.deactivateLookupTable({
lookupTable: lutAddress,
authority: authority.publicKey,
});
const deactivateLutTransaction = new Transaction();
deactivateLutTransaction.add(deactivateInstruction);
await sendAndConfirmTransaction(
connection,
deactivateLutTransaction,
[authority],
{preflightCommitment: 'confirmed'},
);
// After deactivation, LUTs can be closed *only* after a short perioid of time
}).timeout(10 * 1000);
}
});