feat: add getAddressLookupTable method to Connection (#27127)
This commit is contained in:
parent
0ca8239ef7
commit
dcef8ec100
|
@ -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;
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ import type {Struct} from 'superstruct';
|
||||||
import {Client as RpcWebSocketClient} from 'rpc-websockets';
|
import {Client as RpcWebSocketClient} from 'rpc-websockets';
|
||||||
import RpcClient from 'jayson/lib/client/browser';
|
import RpcClient from 'jayson/lib/client/browser';
|
||||||
|
|
||||||
import {URL} from './utils/url-impl';
|
|
||||||
import {AgentManager} from './agent-manager';
|
import {AgentManager} from './agent-manager';
|
||||||
import {EpochSchedule} from './epoch-schedule';
|
import {EpochSchedule} from './epoch-schedule';
|
||||||
import {SendTransactionError, SolanaJSONRPCError} from './errors';
|
import {SendTransactionError, SolanaJSONRPCError} from './errors';
|
||||||
|
@ -35,6 +34,7 @@ import {Signer} from './keypair';
|
||||||
import {MS_PER_SLOT} from './timing';
|
import {MS_PER_SLOT} from './timing';
|
||||||
import {Transaction, TransactionStatus} from './transaction';
|
import {Transaction, TransactionStatus} from './transaction';
|
||||||
import {Message} from './message';
|
import {Message} from './message';
|
||||||
|
import {AddressLookupTableAccount} from './programs/address-lookup-table/state';
|
||||||
import assert from './utils/assert';
|
import assert from './utils/assert';
|
||||||
import {sleep} from './utils/sleep';
|
import {sleep} from './utils/sleep';
|
||||||
import {toBuffer} from './utils/to-buffer';
|
import {toBuffer} from './utils/to-buffer';
|
||||||
|
@ -43,6 +43,7 @@ import {
|
||||||
TransactionExpiredTimeoutError,
|
TransactionExpiredTimeoutError,
|
||||||
} from './transaction/expiry-custom-errors';
|
} from './transaction/expiry-custom-errors';
|
||||||
import {makeWebsocketUrl} from './utils/makeWebsocketUrl';
|
import {makeWebsocketUrl} from './utils/makeWebsocketUrl';
|
||||||
|
import {URL} from './utils/url-impl';
|
||||||
import type {Blockhash} from './blockhash';
|
import type {Blockhash} from './blockhash';
|
||||||
import type {FeeCalculator} from './fee-calculator';
|
import type {FeeCalculator} from './fee-calculator';
|
||||||
import type {TransactionSignature} from './transaction';
|
import type {TransactionSignature} from './transaction';
|
||||||
|
@ -4218,6 +4219,29 @@ export class Connection {
|
||||||
return res.result;
|
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
|
* Fetch the contents of a Nonce account from the cluster, return with context
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import {toBufferLE} from 'bigint-buffer';
|
import {toBufferLE} from 'bigint-buffer';
|
||||||
import * as BufferLayout from '@solana/buffer-layout';
|
import * as BufferLayout from '@solana/buffer-layout';
|
||||||
|
|
||||||
import * as Layout from '../layout';
|
import * as Layout from '../../layout';
|
||||||
import {PublicKey} from '../publickey';
|
import {PublicKey} from '../../publickey';
|
||||||
import * as bigintLayout from '../utils/bigint';
|
import * as bigintLayout from '../../utils/bigint';
|
||||||
import {SystemProgram} from './system';
|
import {SystemProgram} from '../system';
|
||||||
import {TransactionInstruction} from '../transaction';
|
import {TransactionInstruction} from '../../transaction';
|
||||||
import {decodeData, encodeData, IInstructionInputData} from '../instruction';
|
import {decodeData, encodeData, IInstructionInputData} from '../../instruction';
|
||||||
|
|
||||||
|
export * from './state';
|
||||||
|
|
||||||
export type CreateLookupTableParams = {
|
export type CreateLookupTableParams = {
|
||||||
/** Account used to derive and control the new address lookup table. */
|
/** Account used to derive and control the new address lookup table. */
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
};
|
|
@ -18,6 +18,7 @@ import {
|
||||||
sendAndConfirmTransaction,
|
sendAndConfirmTransaction,
|
||||||
Keypair,
|
Keypair,
|
||||||
Message,
|
Message,
|
||||||
|
AddressLookupTableProgram,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import invariant from '../src/utils/assert';
|
import invariant from '../src/utils/assert';
|
||||||
import {MOCK_PORT, url} from './url';
|
import {MOCK_PORT, url} from './url';
|
||||||
|
@ -4243,5 +4244,90 @@ describe('Connection', function () {
|
||||||
const version = await connection.getVersion();
|
const version = await connection.getVersion();
|
||||||
expect(version['solana-core']).to.be.ok;
|
expect(version['solana-core']).to.be.ok;
|
||||||
}).timeout(20 * 1000);
|
}).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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue