import { Buffer } from 'buffer'; import { Commitment, GetProgramAccountsConfig, GetProgramAccountsFilter, ProgramAccountChangeCallback, PublicKey, } from '@solana/web3.js'; import base58 from 'bs58'; import BN from 'bn.js'; import { GmaBuilder, GmaBuilderOptions } from './GmaBuilder'; import { Convergence } from '@/Convergence'; import { UnparsedAccount } from '@/types'; export type GpaSortCallback = ( a: UnparsedAccount, b: UnparsedAccount ) => number; export class GpaBuilder { /** The connection instance to use when fetching accounts. */ protected readonly convergence: Convergence; /** The public key of the program we want to retrieve accounts from. */ protected readonly programId: PublicKey; /** The configs to use when fetching program accounts. */ protected config: GetProgramAccountsConfig = {}; /** When provided, reorder accounts using this callback. */ protected sortCallback?: GpaSortCallback; constructor(convergence: Convergence, programId: PublicKey) { this.convergence = convergence; this.programId = programId; } getFilters() { return this.config.filters; } /** * Subscribes to changes within the program account * based on the specified commitment, defaults to 'confirmed'. * * Returns a subscription id used to unsubscribe */ subscribe(callback: ProgramAccountChangeCallback, commitment?: Commitment) { return this.convergence.connection.onProgramAccountChange( this.programId, callback, commitment ?? 'confirmed', this.config.filters ); } unsubscribe(subscriptionId: number) { return this.convergence.connection.removeProgramAccountChangeListener( subscriptionId ); } mergeConfig(config: GetProgramAccountsConfig) { this.config = { ...this.config, ...config }; return this; } slice(offset: number, length: number) { this.config.dataSlice = { offset, length }; return this; } withoutData() { return this.slice(0, 0); } addFilter(...filters: GetProgramAccountsFilter[]) { if (!this.config.filters) { this.config.filters = []; } this.config.filters.push(...filters); return this; } where(offset: number, bytes: string | Buffer | PublicKey | BN | number) { if (Buffer.isBuffer(bytes)) { bytes = base58.encode(bytes); } else if (typeof bytes === 'object' && 'toBase58' in bytes) { bytes = bytes.toBase58(); } else if (BN.isBN(bytes)) { bytes = base58.encode(bytes.toArray()); } else if (typeof bytes !== 'string') { bytes = base58.encode(new BN(bytes, 'le').toArray()); } return this.addFilter({ memcmp: { offset, bytes } }); } whereSize(dataSize: number) { return this.addFilter({ dataSize }); } sortUsing(callback: GpaSortCallback) { this.sortCallback = callback; return this; } async get(): Promise { const accounts = await this.convergence .rpc() .getProgramAccounts(this.programId, this.config); if (this.sortCallback) { accounts.sort(this.sortCallback); } return accounts; } async getAndMap(callback: (account: UnparsedAccount) => T): Promise { return (await this.get()).map(callback); } async getPublicKeys(): Promise { return this.getAndMap((account) => account.publicKey); } async getDataAsPublicKeys(): Promise { // TODO: Throw a custom ConvergenceRfq error if the data is not a public key. return this.getAndMap((account) => new PublicKey(account.data)); } async getMultipleAccounts( callback?: (account: UnparsedAccount) => PublicKey, options?: GmaBuilderOptions ): Promise { // TODO(loris): Throw a custom Convergence error if the data is not a public key. const cb = callback ?? ((account) => new PublicKey(account.data)); return new GmaBuilder(this.convergence, await this.getAndMap(cb), options); } }