Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

feat: add getAddressLookupTable method to Connection #27127

Merged
merged 1 commit into from
Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact: this change made web3.js incompatible with React Native because:

  • JavaScript Core (JSC) doesn't support the exponentiation operator (**) so there's a transform that converts 2n ** 64n to Math.pow(2n, 64n) and then you're in trouble because Math.pow() doesn't work with bigint (runtime fatal)
  • Hermes supports the exponentiation operator but doesn't support bigint yet.

No change necessary – I'm working on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, thanks for the heads up. Seems worthwhile to use a literal here then, or do you think it's not worth the trouble?

Copy link
Contributor

@steveluscher steveluscher Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are now three places in web3.js that use BigInt

  • This, with exponentiation
  • @noble/ed25519, with exponentiation
  • @noble/secp256k1, with exponentiation

If we inline the computation and convince the noble libs to do the same (eg. just literally change this to BigInt('18446744073709551616') and comment it) then:

  • Less CPU overhead (nice)
  • JSC: still fucked because JSC doesn't support BigInt
  • JSC with BigInt polyfill: still fucked because polyfills don't permit arithmetic (require('big-integer')(1) + require('big-integer')(1) is actually nonsense, because it's basically Object + Object)
  • Pre 0.70 Hermes: still fucked because of no BigInt support
  • >=0.70 Hermes: Works, but so does the original exponentiated code.

I'm going to post about this on September 1st, but my recommendation going forward for anyone who wants to use Solana libraries in React Native going forward will be to upgrade to React Native 0.70 and switch to Hermes. Big integers are critical for most crypto use cases, Hermes is now the default in React Native from 0.70 onward, and the way out of this mess is forward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, really appreciate the breakdown on this. I'll leave as-is then

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