UNPKG

@drift-labs/sdk

Version:
691 lines (627 loc) • 19.4 kB
import { BN } from '@coral-xyz/anchor'; import { User } from '../user'; import { DriftClient } from '../driftClient'; import { UserAccount, OrderRecord, DepositRecord, FundingPaymentRecord, LiquidationRecord, OrderActionRecord, SettlePnlRecord, NewUserRecord, LPRecord, StateAccount, } from '../types'; import { WrappedEvent } from '../events/types'; import { DLOB } from '../dlob/DLOB'; import { UserSubscriptionConfig } from '../userConfig'; import { DataAndSlot, UserEvents } from '../accounts/types'; import { OneShotUserAccountSubscriber } from '../accounts/oneShotUserAccountSubscriber'; import { ProtectMakerParamsMap } from '../dlob/types'; import { Commitment, Connection, MemcmpFilter, PublicKey, RpcResponseAndContext, } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { ZSTDDecoder } from 'zstddec'; import { getNonIdleUserFilter, getUserFilter, getUsersWithPoolId, } from '../memcmp'; import { SyncConfig, UserAccountFilterCriteria as UserFilterCriteria, UserMapConfig, } from './userMapConfig'; import { WebsocketSubscription } from './WebsocketSubscription'; import { PollingSubscription } from './PollingSubscription'; import { decodeUser } from '../decode/user'; import { grpcSubscription } from './grpcSubscription'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; const MAX_USER_ACCOUNT_SIZE_BYTES = 4376; export interface UserMapInterface { eventEmitter: StrictEventEmitter<EventEmitter, UserEvents>; subscribe(): Promise<void>; unsubscribe(): Promise<void>; addPubkey( userAccountPublicKey: PublicKey, userAccount?: UserAccount, slot?: number, accountSubscription?: UserSubscriptionConfig ): Promise<void>; has(key: string): boolean; get(key: string): User | undefined; getWithSlot(key: string): DataAndSlot<User> | undefined; mustGet( key: string, accountSubscription?: UserSubscriptionConfig ): Promise<User>; mustGetWithSlot( key: string, accountSubscription?: UserSubscriptionConfig ): Promise<DataAndSlot<User>>; getUserAuthority(key: string): PublicKey | undefined; updateWithOrderRecord(record: OrderRecord): Promise<void>; values(): IterableIterator<User>; valuesWithSlot(): IterableIterator<DataAndSlot<User>>; entries(): IterableIterator<[string, User]>; entriesWithSlot(): IterableIterator<[string, DataAndSlot<User>]>; } export class UserMap implements UserMapInterface { private userMap = new Map<string, DataAndSlot<User>>(); driftClient: DriftClient; eventEmitter: StrictEventEmitter<EventEmitter, UserEvents>; private connection: Connection; private commitment: Commitment; private includeIdle: boolean; private filterByPoolId?: number; private additionalFilters?: MemcmpFilter[]; private disableSyncOnTotalAccountsChange: boolean; private lastNumberOfSubAccounts: BN; private subscription: | PollingSubscription | WebsocketSubscription | grpcSubscription; private stateAccountUpdateCallback = async (state: StateAccount) => { if (!state.numberOfSubAccounts.eq(this.lastNumberOfSubAccounts)) { await this.sync(); this.lastNumberOfSubAccounts = state.numberOfSubAccounts; } }; private decode; private mostRecentSlot = 0; private syncConfig: SyncConfig; private syncPromise?: Promise<void>; private syncPromiseResolver: () => void; private throwOnFailedSync: boolean; /** * Constructs a new UserMap instance. */ constructor(config: UserMapConfig) { this.driftClient = config.driftClient; if (config.connection) { this.connection = config.connection; } else { this.connection = this.driftClient.connection; } this.commitment = config.subscriptionConfig.type === 'websocket' || config.subscriptionConfig.type === 'polling' ? config.subscriptionConfig.commitment ?? this.driftClient.opts.commitment : this.driftClient.opts.commitment; this.includeIdle = config.includeIdle ?? false; this.filterByPoolId = config.filterByPoolId; this.additionalFilters = config.additionalFilters; this.disableSyncOnTotalAccountsChange = config.disableSyncOnTotalAccountsChange ?? false; let decodeFn; if (config.fastDecode ?? true) { decodeFn = (name, buffer) => decodeUser(buffer); } else { decodeFn = this.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind( this.driftClient.program.account.user.coder.accounts ); } this.decode = decodeFn; if (config.subscriptionConfig.type === 'polling') { this.subscription = new PollingSubscription({ userMap: this, frequency: config.subscriptionConfig.frequency, skipInitialLoad: config.skipInitialLoad, }); } else if (config.subscriptionConfig.type === 'grpc') { this.subscription = new grpcSubscription({ userMap: this, grpcConfigs: config.subscriptionConfig.grpcConfigs, resubOpts: { resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs, logResubMessages: config.subscriptionConfig.logResubMessages, }, skipInitialLoad: config.skipInitialLoad, decodeFn, }); } else { this.subscription = new WebsocketSubscription({ userMap: this, commitment: this.commitment, resubOpts: { resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs, logResubMessages: config.subscriptionConfig.logResubMessages, }, skipInitialLoad: config.skipInitialLoad, decodeFn, }); } this.syncConfig = config.syncConfig ?? { type: 'default', }; // Whether to throw an error if the userMap fails to sync. Defaults to false. this.throwOnFailedSync = config.throwOnFailedSync ?? false; this.eventEmitter = new EventEmitter(); } public async subscribe() { if (this.size() > 0) { return; } await this.driftClient.subscribe(); this.lastNumberOfSubAccounts = this.driftClient.getStateAccount().numberOfSubAccounts; if (!this.disableSyncOnTotalAccountsChange) { this.driftClient.eventEmitter.on( 'stateAccountUpdate', this.stateAccountUpdateCallback ); } await this.subscription.subscribe(); } public async addPubkey( userAccountPublicKey: PublicKey, userAccount?: UserAccount, slot?: number, accountSubscription?: UserSubscriptionConfig ) { const user = new User({ driftClient: this.driftClient, userAccountPublicKey, accountSubscription: accountSubscription ?? { type: 'custom', // OneShotUserAccountSubscriber used here so we don't load up the RPC with AccountSubscribes userAccountSubscriber: new OneShotUserAccountSubscriber( this.driftClient.program, userAccountPublicKey, userAccount, slot, this.commitment ), }, }); await user.subscribe(userAccount); this.userMap.set(userAccountPublicKey.toString(), { data: user, slot: slot ?? user.getUserAccountAndSlot()?.slot, }); this.eventEmitter.emit('userUpdate', user); } public has(key: string): boolean { return this.userMap.has(key); } /** * gets the User for a particular userAccountPublicKey, if no User exists, undefined is returned * @param key userAccountPublicKey to get User for * @returns user User | undefined */ public get(key: string): User | undefined { return this.userMap.get(key)?.data; } public getWithSlot(key: string): DataAndSlot<User> | undefined { return this.userMap.get(key); } /** * gets the User for a particular userAccountPublicKey, if no User exists, new one is created * @param key userAccountPublicKey to get User for * @returns User */ public async mustGet( key: string, accountSubscription?: UserSubscriptionConfig ): Promise<User> { if (!this.has(key)) { await this.addPubkey( new PublicKey(key), undefined, undefined, accountSubscription ); } return this.userMap.get(key).data; } public async mustGetWithSlot( key: string, accountSubscription?: UserSubscriptionConfig ): Promise<DataAndSlot<User>> { if (!this.has(key)) { await this.addPubkey( new PublicKey(key), undefined, undefined, accountSubscription ); } return this.userMap.get(key); } public async mustGetUserAccount(key: string): Promise<UserAccount> { const user = await this.mustGet(key); return user.getUserAccount(); } /** * gets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned * @param key userAccountPublicKey to get User for * @returns authority PublicKey | undefined */ public getUserAuthority(key: string): PublicKey | undefined { const user = this.userMap.get(key); if (!user) { return undefined; } return user.data.getUserAccount().authority; } /** * implements the {@link DLOBSource} interface * create a DLOB from all the subscribed users * @param slot */ public async getDLOB( slot: number, protectedMakerParamsMap?: ProtectMakerParamsMap ): Promise<DLOB> { const dlob = new DLOB(protectedMakerParamsMap); await dlob.initFromUserMap(this, slot); return dlob; } public async updateWithOrderRecord(record: OrderRecord) { if (!this.has(record.user.toString())) { await this.addPubkey(record.user); } } public async updateWithEventRecord(record: WrappedEvent<any>) { if (record.eventType === 'DepositRecord') { const depositRecord = record as DepositRecord; await this.mustGet(depositRecord.user.toString()); } else if (record.eventType === 'FundingPaymentRecord') { const fundingPaymentRecord = record as FundingPaymentRecord; await this.mustGet(fundingPaymentRecord.user.toString()); } else if (record.eventType === 'LiquidationRecord') { const liqRecord = record as LiquidationRecord; await this.mustGet(liqRecord.user.toString()); await this.mustGet(liqRecord.liquidator.toString()); } else if (record.eventType === 'OrderRecord') { const orderRecord = record as OrderRecord; await this.updateWithOrderRecord(orderRecord); } else if (record.eventType === 'OrderActionRecord') { const actionRecord = record as OrderActionRecord; if (actionRecord.taker) { await this.mustGet(actionRecord.taker.toString()); } if (actionRecord.maker) { await this.mustGet(actionRecord.maker.toString()); } } else if (record.eventType === 'SettlePnlRecord') { const settlePnlRecord = record as SettlePnlRecord; await this.mustGet(settlePnlRecord.user.toString()); } else if (record.eventType === 'NewUserRecord') { const newUserRecord = record as NewUserRecord; await this.mustGet(newUserRecord.user.toString()); } else if (record.eventType === 'LPRecord') { const lpRecord = record as LPRecord; await this.mustGet(lpRecord.user.toString()); } } public *values(): IterableIterator<User> { for (const dataAndSlot of this.userMap.values()) { yield dataAndSlot.data; } } public valuesWithSlot(): IterableIterator<DataAndSlot<User>> { return this.userMap.values(); } public *entries(): IterableIterator<[string, User]> { for (const [key, dataAndSlot] of this.userMap.entries()) { yield [key, dataAndSlot.data]; } } public entriesWithSlot(): IterableIterator<[string, DataAndSlot<User>]> { return this.userMap.entries(); } public size(): number { return this.userMap.size; } /** * Returns a unique list of authorities for all users in the UserMap that meet the filter criteria * @param filterCriteria: Users must meet these criteria to be included * @returns */ public getUniqueAuthorities( filterCriteria?: UserFilterCriteria ): PublicKey[] { const usersMeetingCriteria = Array.from(this.values()).filter((user) => { let pass = true; if (filterCriteria && filterCriteria.hasOpenOrders) { pass = pass && user.getUserAccount().hasOpenOrder; } return pass; }); const userAuths = new Set( usersMeetingCriteria.map((user) => user.getUserAccount().authority.toBase58() ) ); const userAuthKeys = Array.from(userAuths).map( (userAuth) => new PublicKey(userAuth) ); return userAuthKeys; } public async sync() { if (this.syncConfig.type === 'default') { return this.defaultSync(); } else { return this.paginatedSync(); } } private getFilters(): MemcmpFilter[] { const filters = [getUserFilter()]; if (!this.includeIdle) { filters.push(getNonIdleUserFilter()); } if (this.filterByPoolId !== undefined) { filters.push(getUsersWithPoolId(this.filterByPoolId)); } if (this.additionalFilters) { filters.push(...this.additionalFilters); } return filters; } /** * Syncs the UserMap using the default sync method (single getProgramAccounts call with filters). * This method may fail when drift has too many users. (nodejs response size limits) * @returns */ private async defaultSync() { if (this.syncPromise) { return this.syncPromise; } this.syncPromise = new Promise((resolver) => { this.syncPromiseResolver = resolver; }); try { const rpcRequestArgs = [ this.driftClient.program.programId.toBase58(), { commitment: this.commitment, filters: this.getFilters(), encoding: 'base64+zstd', withContext: true, }, ]; // @ts-ignore const rpcJSONResponse: any = await this.connection._rpcRequest( 'getProgramAccounts', rpcRequestArgs ); const rpcResponseAndContext: RpcResponseAndContext< Array<{ pubkey: PublicKey; account: { data: [string, string] } }> > = rpcJSONResponse.result; const slot = rpcResponseAndContext.context.slot; this.updateLatestSlot(slot); const programAccountBufferMap = new Map<string, Buffer>(); const decodingPromises = rpcResponseAndContext.value.map( async (programAccount) => { const compressedUserData = Buffer.from( programAccount.account.data[0], 'base64' ); const decoder = new ZSTDDecoder(); await decoder.init(); const userBuffer = decoder.decode( compressedUserData, MAX_USER_ACCOUNT_SIZE_BYTES ); programAccountBufferMap.set( programAccount.pubkey.toString(), Buffer.from(userBuffer) ); } ); await Promise.all(decodingPromises); const promises = Array.from(programAccountBufferMap.entries()).map( ([key, buffer]) => (async () => { const currAccountWithSlot = this.getWithSlot(key); if (currAccountWithSlot) { if (slot >= currAccountWithSlot.slot) { const userAccount = this.decode('User', buffer); this.updateUserAccount(key, userAccount, slot); } } else { const userAccount = this.decode('User', buffer); await this.addPubkey(new PublicKey(key), userAccount, slot); } })() ); await Promise.all(promises); for (const [key] of this.entries()) { if (!programAccountBufferMap.has(key)) { const user = this.get(key); if (user) { await user.unsubscribe(); this.userMap.delete(key); } } } } catch (err) { const e = err as Error; console.error(`Error in UserMap.sync(): ${e.message} ${e.stack ?? ''}`); if (this.throwOnFailedSync) { throw e; } } finally { this.syncPromiseResolver(); this.syncPromise = undefined; } } /** * Syncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters). * This method is more reliable when drift has many users. * @returns */ private async paginatedSync() { if (this.syncPromise) { return this.syncPromise; } this.syncPromise = new Promise<void>((resolve) => { this.syncPromiseResolver = resolve; }); try { const accountsPrefetch = await this.connection.getProgramAccounts( this.driftClient.program.programId, { dataSlice: { offset: 0, length: 0 }, filters: this.getFilters(), } ); const accountPublicKeys = accountsPrefetch.map( (account) => account.pubkey ); const limitConcurrency = async (tasks, limit) => { const executing = []; const results = []; for (let i = 0; i < tasks.length; i++) { const executor = Promise.resolve().then(tasks[i]); results.push(executor); if (executing.length < limit) { executing.push(executor); executor.finally(() => { const index = executing.indexOf(executor); if (index > -1) { executing.splice(index, 1); } }); } else { await Promise.race(executing); } } return Promise.all(results); }; const programAccountBufferMap = new Map<string, Buffer>(); // @ts-ignore const chunkSize = this.syncConfig.chunkSize ?? 100; const tasks = []; for (let i = 0; i < accountPublicKeys.length; i += chunkSize) { const chunk = accountPublicKeys.slice(i, i + chunkSize); tasks.push(async () => { const accountInfos = await this.connection.getMultipleAccountsInfoAndContext(chunk, { commitment: this.commitment, }); const accountInfosSlot = accountInfos.context.slot; for (let j = 0; j < accountInfos.value.length; j += 1) { const accountInfo = accountInfos.value[j]; if (accountInfo === null) continue; const publicKeyString = chunk[j].toString(); const buffer = Buffer.from(accountInfo.data); programAccountBufferMap.set(publicKeyString, buffer); const decodedUser = this.decode('User', buffer); const currAccountWithSlot = this.getWithSlot(publicKeyString); if ( currAccountWithSlot && currAccountWithSlot.slot <= accountInfosSlot ) { this.updateUserAccount( publicKeyString, decodedUser, accountInfosSlot ); } else { await this.addPubkey( new PublicKey(publicKeyString), decodedUser, accountInfosSlot ); } } }); } // @ts-ignore const concurrencyLimit = this.syncConfig.concurrencyLimit ?? 10; await limitConcurrency(tasks, concurrencyLimit); for (const [key] of this.entries()) { if (!programAccountBufferMap.has(key)) { const user = this.get(key); if (user) { await user.unsubscribe(); this.userMap.delete(key); } } } } catch (err) { console.error(`Error in UserMap.sync():`, err); if (this.throwOnFailedSync) { throw err; } } finally { if (this.syncPromiseResolver) { this.syncPromiseResolver(); } this.syncPromise = undefined; } } public async unsubscribe() { await this.subscription.unsubscribe(); for (const [key, user] of this.entries()) { await user.unsubscribe(); this.userMap.delete(key); } if (this.lastNumberOfSubAccounts) { if (!this.disableSyncOnTotalAccountsChange) { this.driftClient.eventEmitter.removeListener( 'stateAccountUpdate', this.stateAccountUpdateCallback ); } this.lastNumberOfSubAccounts = undefined; } } public async updateUserAccount( key: string, userAccount: UserAccount, slot: number ) { const userWithSlot = this.getWithSlot(key); this.updateLatestSlot(slot); if (userWithSlot) { if (slot >= userWithSlot.slot) { userWithSlot.data.accountSubscriber.updateData(userAccount, slot); this.userMap.set(key, { data: userWithSlot.data, slot, }); this.eventEmitter.emit('userUpdate', userWithSlot.data); } } else { this.addPubkey(new PublicKey(key), userAccount, slot); } } updateLatestSlot(slot: number): void { this.mostRecentSlot = Math.max(slot, this.mostRecentSlot); } public getSlot(): number { return this.mostRecentSlot; } }