UNPKG

@drift-labs/sdk

Version:
276 lines (275 loc) • 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UserStatsMap = void 0; const pda_1 = require("../addresses/pda"); const userStats_1 = require("../userStats"); const bulkAccountLoader_1 = require("../accounts/bulkAccountLoader"); const memcmp_1 = require("../memcmp"); const web3_js_1 = require("@solana/web3.js"); class UserStatsMap { /** * 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, bulkAccountLoader, syncConfig) { /** * map from authority pubkey to UserStats */ this.userStatsMap = new Map(); this.driftClient = driftClient; if (!bulkAccountLoader) { bulkAccountLoader = new bulkAccountLoader_1.BulkAccountLoader(driftClient.connection, driftClient.opts.commitment, 0); } this.bulkAccountLoader = bulkAccountLoader; this.syncConfig = syncConfig !== null && syncConfig !== void 0 ? syncConfig : { type: 'default', }; this.decode = this.driftClient.program.account.userStats.coder.accounts.decodeUnchecked.bind(this.driftClient.program.account.userStats.coder.accounts); } async subscribe(authorities) { 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 */ async addUserStat(authority, userStatsAccount, skipFetch) { const userStat = new userStats_1.UserStats({ driftClient: this.driftClient, userStatsAccountPublicKey: (0, pda_1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, authority), accountSubscription: { type: 'polling', accountLoader: this.bulkAccountLoader, }, }); if (skipFetch) { await userStat.accountSubscriber.addToAccountLoader(); } else { await userStat.subscribe(userStatsAccount); } this.userStatsMap.set(authority.toString(), userStat); } async updateWithOrderRecord(record, userMap) { const user = await userMap.mustGet(record.user.toString()); if (!this.has(user.getUserAccount().authority.toString())) { await this.addUserStat(user.getUserAccount().authority, undefined, false); } } async updateWithEventRecord(record, userMap) { if (record.eventType === 'DepositRecord') { const depositRecord = record; await this.mustGet(depositRecord.userAuthority.toString()); } else if (record.eventType === 'FundingPaymentRecord') { const fundingPaymentRecord = record; await this.mustGet(fundingPaymentRecord.userAuthority.toString()); } else if (record.eventType === 'LiquidationRecord') { if (!userMap) { return; } const liqRecord = record; 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; await userMap.updateWithOrderRecord(orderRecord); } else if (record.eventType === 'OrderActionRecord') { if (!userMap) { return; } const actionRecord = record; 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; const user = await userMap.mustGet(settlePnlRecord.user.toString()); await this.mustGet(user.getUserAccount().authority.toString()); } else if (record.eventType === 'NewUserRecord') { const newUserRecord = record; await this.mustGet(newUserRecord.userAuthority.toString()); } else if (record.eventType === 'LPRecord') { if (!userMap) { return; } const lpRecord = record; const user = await userMap.mustGet(lpRecord.user.toString()); await this.mustGet(user.getUserAccount().authority.toString()); } else if (record.eventType === 'InsuranceFundStakeRecord') { const ifStakeRecord = record; await this.mustGet(ifStakeRecord.userAuthority.toString()); } } has(authorityPublicKey) { return this.userStatsMap.has(authorityPublicKey); } get(authorityPublicKey) { 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 */ async mustGet(authorityPublicKey) { if (!this.has(authorityPublicKey)) { await this.addUserStat(new web3_js_1.PublicKey(authorityPublicKey), undefined, false); } return this.get(authorityPublicKey); } values() { return this.userStatsMap.values(); } size() { 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 */ async sync(authorities) { 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 */ async defaultSync(authorities) { 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 */ async paginatedSync(authorities) { var _a, _b; if (this.syncPromise) { return this.syncPromise; } this.syncPromise = new Promise((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: [(0, memcmp_1.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(); // @ts-ignore const chunkSize = (_a = this.syncConfig.chunkSize) !== null && _a !== void 0 ? _a : 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); programAccountBufferMap.add(decodedUserStats.authority.toBase58()); this.addUserStat(decodedUserStats.authority, decodedUserStats, false); } } }); } // @ts-ignore const concurrencyLimit = (_b = this.syncConfig.concurrencyLimit) !== null && _b !== void 0 ? _b : 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; } } async unsubscribe() { for (const [key, userStats] of this.userStatsMap.entries()) { await userStats.unsubscribe(); this.userStatsMap.delete(key); } } } exports.UserStatsMap = UserStatsMap;