UNPKG

@drift-labs/sdk

Version:
366 lines (328 loc) • 10.7 kB
import { DriftClient } from '../driftClient'; import { getUserStatsAccountPublicKey } from '../addresses/pda'; import { OrderRecord, UserStatsAccount, DepositRecord, FundingPaymentRecord, LiquidationRecord, OrderActionRecord, SettlePnlRecord, NewUserRecord, LPRecord, InsuranceFundStakeRecord, } from '../types'; import { UserStats } from '../userStats'; import { WrappedEvent } from '../events/types'; import { BulkAccountLoader } from '../accounts/bulkAccountLoader'; import { PollingUserStatsAccountSubscriber } from '../accounts/pollingUserStatsAccountSubscriber'; import { SyncConfig } from './userMapConfig'; import { getUserStatsFilter } from '../memcmp'; import { PublicKey } from '@solana/web3.js'; import { UserMap } from './userMap'; export class UserStatsMap { /** * map from authority pubkey to UserStats */ private userStatsMap = new Map<string, UserStats>(); private driftClient: DriftClient; private bulkAccountLoader: BulkAccountLoader; private decode; private syncConfig: SyncConfig; private syncPromise?: Promise<void>; private syncPromiseResolver: () => void; /** * Creates a new UserStatsMap instance. * * @param {DriftClient} driftClient - The DriftClient instance. * @param {BulkAccountLoader} [bulkAccountLoader] - If not provided, a new BulkAccountLoader with polling disabled will be created. */ constructor( driftClient: DriftClient, bulkAccountLoader?: BulkAccountLoader, syncConfig?: SyncConfig ) { this.driftClient = driftClient; if (!bulkAccountLoader) { bulkAccountLoader = new BulkAccountLoader( driftClient.connection, driftClient.opts.commitment, 0 ); } this.bulkAccountLoader = bulkAccountLoader; this.syncConfig = syncConfig ?? { type: 'default', }; this.decode = this.driftClient.program.account.userStats.coder.accounts.decodeUnchecked.bind( this.driftClient.program.account.userStats.coder.accounts ); } public async subscribe(authorities: PublicKey[]) { if (this.size() > 0) { return; } await this.driftClient.subscribe(); await this.sync(authorities); } /** * * @param authority that owns the UserStatsAccount * @param userStatsAccount optional UserStatsAccount to subscribe to, if undefined will be fetched later * @param skipFetch if true, will not immediately fetch the UserStatsAccount */ public async addUserStat( authority: PublicKey, userStatsAccount?: UserStatsAccount, skipFetch?: boolean ) { const userStat = new UserStats({ driftClient: this.driftClient, userStatsAccountPublicKey: getUserStatsAccountPublicKey( this.driftClient.program.programId, authority ), accountSubscription: { type: 'polling', accountLoader: this.bulkAccountLoader, }, }); if (skipFetch) { await ( userStat.accountSubscriber as PollingUserStatsAccountSubscriber ).addToAccountLoader(); } else { await userStat.subscribe(userStatsAccount); } this.userStatsMap.set(authority.toString(), userStat); } public async updateWithOrderRecord(record: OrderRecord, userMap: UserMap) { const user = await userMap.mustGet(record.user.toString()); if (!this.has(user.getUserAccount().authority.toString())) { await this.addUserStat(user.getUserAccount().authority, undefined, false); } } public async updateWithEventRecord( record: WrappedEvent<any>, userMap?: UserMap ) { if (record.eventType === 'DepositRecord') { const depositRecord = record as DepositRecord; await this.mustGet(depositRecord.userAuthority.toString()); } else if (record.eventType === 'FundingPaymentRecord') { const fundingPaymentRecord = record as FundingPaymentRecord; await this.mustGet(fundingPaymentRecord.userAuthority.toString()); } else if (record.eventType === 'LiquidationRecord') { if (!userMap) { return; } const liqRecord = record as LiquidationRecord; const user = await userMap.mustGet(liqRecord.user.toString()); await this.mustGet(user.getUserAccount().authority.toString()); const liquidatorUser = await userMap.mustGet( liqRecord.liquidator.toString() ); await this.mustGet(liquidatorUser.getUserAccount().authority.toString()); } else if (record.eventType === 'OrderRecord') { if (!userMap) { return; } const orderRecord = record as OrderRecord; await userMap.updateWithOrderRecord(orderRecord); } else if (record.eventType === 'OrderActionRecord') { if (!userMap) { return; } const actionRecord = record as OrderActionRecord; if (actionRecord.taker) { const taker = await userMap.mustGet(actionRecord.taker.toString()); await this.mustGet(taker.getUserAccount().authority.toString()); } if (actionRecord.maker) { const maker = await userMap.mustGet(actionRecord.maker.toString()); await this.mustGet(maker.getUserAccount().authority.toString()); } } else if (record.eventType === 'SettlePnlRecord') { if (!userMap) { return; } const settlePnlRecord = record as SettlePnlRecord; const user = await userMap.mustGet(settlePnlRecord.user.toString()); await this.mustGet(user.getUserAccount().authority.toString()); } else if (record.eventType === 'NewUserRecord') { const newUserRecord = record as NewUserRecord; await this.mustGet(newUserRecord.userAuthority.toString()); } else if (record.eventType === 'LPRecord') { if (!userMap) { return; } const lpRecord = record as LPRecord; const user = await userMap.mustGet(lpRecord.user.toString()); await this.mustGet(user.getUserAccount().authority.toString()); } else if (record.eventType === 'InsuranceFundStakeRecord') { const ifStakeRecord = record as InsuranceFundStakeRecord; await this.mustGet(ifStakeRecord.userAuthority.toString()); } } public has(authorityPublicKey: string): boolean { return this.userStatsMap.has(authorityPublicKey); } public get(authorityPublicKey: string): UserStats { return this.userStatsMap.get(authorityPublicKey); } /** * Enforce that a UserStats will exist for the given authorityPublicKey, * reading one from the blockchain if necessary. * @param authorityPublicKey * @returns */ public async mustGet(authorityPublicKey: string): Promise<UserStats> { if (!this.has(authorityPublicKey)) { await this.addUserStat( new PublicKey(authorityPublicKey), undefined, false ); } return this.get(authorityPublicKey); } public values(): IterableIterator<UserStats> { return this.userStatsMap.values(); } public size(): number { return this.userStatsMap.size; } /** * Sync the UserStatsMap * @param authorities list of authorities to derive UserStatsAccount public keys from. * You may want to get this list from UserMap in order to filter out idle users */ public async sync(authorities: PublicKey[]) { if (this.syncConfig.type === 'default') { return this.defaultSync(authorities); } else { return this.paginatedSync(authorities); } } /** * Sync the UserStatsMap using the default sync method, which loads individual users into the bulkAccountLoader and * loads them. (bulkAccountLoader uses batch getMultipleAccounts) * @param authorities */ private async defaultSync(authorities: PublicKey[]) { await Promise.all( authorities.map((authority) => this.addUserStat(authority, undefined, true) ) ); await this.bulkAccountLoader.load(); } /** * Sync the UserStatsMap using the paginated sync method, which uses multiple getMultipleAccounts calls (without RPC batching), and limits concurrency. * @param authorities */ private async paginatedSync(authorities: PublicKey[]) { if (this.syncPromise) { return this.syncPromise; } this.syncPromise = new Promise<void>((resolve) => { this.syncPromiseResolver = resolve; }); try { let accountsToLoad = authorities; if (authorities.length === 0) { const accountsPrefetch = await this.driftClient.connection.getProgramAccounts( this.driftClient.program.programId, { dataSlice: { offset: 0, length: 0 }, filters: [getUserStatsFilter()], } ); accountsToLoad = 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 Set<string>(); // @ts-ignore const chunkSize = this.syncConfig.chunkSize ?? 100; const tasks = []; for (let i = 0; i < accountsToLoad.length; i += chunkSize) { const chunk = accountsToLoad.slice(i, i + chunkSize); tasks.push(async () => { const accountInfos = await this.driftClient.connection.getMultipleAccountsInfoAndContext( chunk, { commitment: this.driftClient.opts.commitment, } ); for (let j = 0; j < accountInfos.value.length; j += 1) { const accountInfo = accountInfos.value[j]; if (accountInfo === null) continue; const publicKeyString = chunk[j].toString(); if (!this.has(publicKeyString)) { const buffer = Buffer.from(accountInfo.data); const decodedUserStats = this.decode( 'UserStats', buffer ) as UserStatsAccount; programAccountBufferMap.add( decodedUserStats.authority.toBase58() ); this.addUserStat( decodedUserStats.authority, decodedUserStats, false ); } } }); } // @ts-ignore const concurrencyLimit = this.syncConfig.concurrencyLimit ?? 10; await limitConcurrency(tasks, concurrencyLimit); for (const [key] of this.userStatsMap.entries()) { if (!programAccountBufferMap.has(key)) { const user = this.get(key); if (user) { await user.unsubscribe(); this.userStatsMap.delete(key); } } } } catch (err) { console.error(`Error in UserStatsMap.paginatedSync():`, err); } finally { if (this.syncPromiseResolver) { this.syncPromiseResolver(); } this.syncPromise = undefined; } } public async unsubscribe() { for (const [key, userStats] of this.userStatsMap.entries()) { await userStats.unsubscribe(); this.userStatsMap.delete(key); } } }