feat: add getAddressLookupTable method to Connection (#27127)

This commit is contained in:
Justin Starry 2022-08-14 11:11:49 +01:00 committed by GitHub
parent 0ca8239ef7
commit dcef8ec100
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 242 additions and 7 deletions

View File

@ -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;
}

View File

@ -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<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
*/

View File

@ -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. */

View File

@ -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',
),
]),
};

View File

@ -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;
}
});
}
});