UNPKG

@drift-labs/sdk

Version:
492 lines (491 loc) • 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UserMap = void 0; const user_1 = require("../user"); const DLOB_1 = require("../dlob/DLOB"); const oneShotUserAccountSubscriber_1 = require("../accounts/oneShotUserAccountSubscriber"); const web3_js_1 = require("@solana/web3.js"); const buffer_1 = require("buffer"); const zstddec_1 = require("zstddec"); const memcmp_1 = require("../memcmp"); const WebsocketSubscription_1 = require("./WebsocketSubscription"); const PollingSubscription_1 = require("./PollingSubscription"); const user_2 = require("../decode/user"); const grpcSubscription_1 = require("./grpcSubscription"); const events_1 = require("events"); const MAX_USER_ACCOUNT_SIZE_BYTES = 4376; class UserMap { /** * Constructs a new UserMap instance. */ constructor(config) { var _a, _b, _c, _d, _e, _f; this.userMap = new Map(); this.stateAccountUpdateCallback = async (state) => { if (!state.numberOfSubAccounts.eq(this.lastNumberOfSubAccounts)) { await this.sync(); this.lastNumberOfSubAccounts = state.numberOfSubAccounts; } }; this.mostRecentSlot = 0; 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' ? (_a = config.subscriptionConfig.commitment) !== null && _a !== void 0 ? _a : this.driftClient.opts.commitment : this.driftClient.opts.commitment; this.includeIdle = (_b = config.includeIdle) !== null && _b !== void 0 ? _b : false; this.filterByPoolId = config.filterByPoolId; this.additionalFilters = config.additionalFilters; this.disableSyncOnTotalAccountsChange = (_c = config.disableSyncOnTotalAccountsChange) !== null && _c !== void 0 ? _c : false; let decodeFn; if ((_d = config.fastDecode) !== null && _d !== void 0 ? _d : true) { decodeFn = (name, buffer) => (0, user_2.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_1.PollingSubscription({ userMap: this, frequency: config.subscriptionConfig.frequency, skipInitialLoad: config.skipInitialLoad, }); } else if (config.subscriptionConfig.type === 'grpc') { this.subscription = new grpcSubscription_1.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_1.WebsocketSubscription({ userMap: this, commitment: this.commitment, resubOpts: { resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs, logResubMessages: config.subscriptionConfig.logResubMessages, }, skipInitialLoad: config.skipInitialLoad, decodeFn, }); } this.syncConfig = (_e = config.syncConfig) !== null && _e !== void 0 ? _e : { type: 'default', }; // Whether to throw an error if the userMap fails to sync. Defaults to false. this.throwOnFailedSync = (_f = config.throwOnFailedSync) !== null && _f !== void 0 ? _f : false; this.eventEmitter = new events_1.EventEmitter(); } 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(); } async addPubkey(userAccountPublicKey, userAccount, slot, accountSubscription) { var _a; const user = new user_1.User({ driftClient: this.driftClient, userAccountPublicKey, accountSubscription: accountSubscription !== null && accountSubscription !== void 0 ? accountSubscription : { type: 'custom', // OneShotUserAccountSubscriber used here so we don't load up the RPC with AccountSubscribes userAccountSubscriber: new oneShotUserAccountSubscriber_1.OneShotUserAccountSubscriber(this.driftClient.program, userAccountPublicKey, userAccount, slot, this.commitment), }, }); await user.subscribe(userAccount); this.userMap.set(userAccountPublicKey.toString(), { data: user, slot: slot !== null && slot !== void 0 ? slot : (_a = user.getUserAccountAndSlot()) === null || _a === void 0 ? void 0 : _a.slot, }); this.eventEmitter.emit('userUpdate', user); } has(key) { 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 */ get(key) { var _a; return (_a = this.userMap.get(key)) === null || _a === void 0 ? void 0 : _a.data; } getWithSlot(key) { 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 */ async mustGet(key, accountSubscription) { if (!this.has(key)) { await this.addPubkey(new web3_js_1.PublicKey(key), undefined, undefined, accountSubscription); } return this.userMap.get(key).data; } async mustGetWithSlot(key, accountSubscription) { if (!this.has(key)) { await this.addPubkey(new web3_js_1.PublicKey(key), undefined, undefined, accountSubscription); } return this.userMap.get(key); } async mustGetUserAccount(key) { 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 */ getUserAuthority(key) { 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 */ async getDLOB(slot, protectedMakerParamsMap) { const dlob = new DLOB_1.DLOB(protectedMakerParamsMap); await dlob.initFromUserMap(this, slot); return dlob; } async updateWithOrderRecord(record) { if (!this.has(record.user.toString())) { await this.addPubkey(record.user); } } async updateWithEventRecord(record) { if (record.eventType === 'DepositRecord') { const depositRecord = record; await this.mustGet(depositRecord.user.toString()); } else if (record.eventType === 'FundingPaymentRecord') { const fundingPaymentRecord = record; await this.mustGet(fundingPaymentRecord.user.toString()); } else if (record.eventType === 'LiquidationRecord') { const liqRecord = record; await this.mustGet(liqRecord.user.toString()); await this.mustGet(liqRecord.liquidator.toString()); } else if (record.eventType === 'OrderRecord') { const orderRecord = record; await this.updateWithOrderRecord(orderRecord); } else if (record.eventType === 'OrderActionRecord') { const actionRecord = record; 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; await this.mustGet(settlePnlRecord.user.toString()); } else if (record.eventType === 'NewUserRecord') { const newUserRecord = record; await this.mustGet(newUserRecord.user.toString()); } else if (record.eventType === 'LPRecord') { const lpRecord = record; await this.mustGet(lpRecord.user.toString()); } } *values() { for (const dataAndSlot of this.userMap.values()) { yield dataAndSlot.data; } } valuesWithSlot() { return this.userMap.values(); } *entries() { for (const [key, dataAndSlot] of this.userMap.entries()) { yield [key, dataAndSlot.data]; } } entriesWithSlot() { return this.userMap.entries(); } size() { 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 */ getUniqueAuthorities(filterCriteria) { 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 web3_js_1.PublicKey(userAuth)); return userAuthKeys; } async sync() { if (this.syncConfig.type === 'default') { return this.defaultSync(); } else { return this.paginatedSync(); } } getFilters() { const filters = [(0, memcmp_1.getUserFilter)()]; if (!this.includeIdle) { filters.push((0, memcmp_1.getNonIdleUserFilter)()); } if (this.filterByPoolId !== undefined) { filters.push((0, memcmp_1.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 */ async defaultSync() { var _a; 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 = await this.connection._rpcRequest('getProgramAccounts', rpcRequestArgs); const rpcResponseAndContext = rpcJSONResponse.result; const slot = rpcResponseAndContext.context.slot; this.updateLatestSlot(slot); const programAccountBufferMap = new Map(); const decodingPromises = rpcResponseAndContext.value.map(async (programAccount) => { const compressedUserData = buffer_1.Buffer.from(programAccount.account.data[0], 'base64'); const decoder = new zstddec_1.ZSTDDecoder(); await decoder.init(); const userBuffer = decoder.decode(compressedUserData, MAX_USER_ACCOUNT_SIZE_BYTES); programAccountBufferMap.set(programAccount.pubkey.toString(), buffer_1.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 web3_js_1.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; console.error(`Error in UserMap.sync(): ${e.message} ${(_a = e.stack) !== null && _a !== void 0 ? _a : ''}`); 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 */ async paginatedSync() { var _a, _b; if (this.syncPromise) { return this.syncPromise; } this.syncPromise = new Promise((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(); // @ts-ignore const chunkSize = (_a = this.syncConfig.chunkSize) !== null && _a !== void 0 ? _a : 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_1.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 web3_js_1.PublicKey(publicKeyString), decodedUser, accountInfosSlot); } } }); } // @ts-ignore const concurrencyLimit = (_b = this.syncConfig.concurrencyLimit) !== null && _b !== void 0 ? _b : 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; } } 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; } } async updateUserAccount(key, userAccount, slot) { 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 web3_js_1.PublicKey(key), userAccount, slot); } } updateLatestSlot(slot) { this.mostRecentSlot = Math.max(slot, this.mostRecentSlot); } getSlot() { return this.mostRecentSlot; } } exports.UserMap = UserMap;