UNPKG

@drift-labs/sdk

Version:
445 lines (444 loc) • 23.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketDriftClientAccountSubscriberV2 = void 0; const types_1 = require("./types"); const types_2 = require("../types"); const events_1 = require("events"); const pda_1 = require("../addresses/pda"); const web3_js_1 = require("@solana/web3.js"); const gill_1 = require("gill"); const oracleClientCache_1 = require("../oracles/oracleClientCache"); const quoteAssetOracleClient_1 = require("../oracles/quoteAssetOracleClient"); const config_1 = require("../config"); const utils_1 = require("./utils"); const oracleId_1 = require("../oracles/oracleId"); const types_3 = require("../types"); const memcmp_1 = require("../memcmp"); const webSocketProgramAccountsSubscriberV2_1 = require("./webSocketProgramAccountsSubscriberV2"); const webSocketAccountSubscriberV2_1 = require("./webSocketAccountSubscriberV2"); const ORACLE_DEFAULT_ID = (0, oracleId_1.getOracleId)(web3_js_1.PublicKey.default, types_3.OracleSource.QUOTE_ASSET); class WebSocketDriftClientAccountSubscriberV2 { constructor(program, perpMarketIndexes, spotMarketIndexes, oracleInfos, shouldFindAllMarketsAndOracles, delistedMarketSetting, resubOpts, commitment, skipInitialData) { this.oracleClientCache = new oracleClientCache_1.OracleClientCache(); this.skipInitialData = true; this.perpMarketAccountLatestData = new Map(); this.spotMarketAccountLatestData = new Map(); this.perpOracleMap = new Map(); this.perpOracleStringMap = new Map(); this.spotOracleMap = new Map(); this.spotOracleStringMap = new Map(); this.oracleSubscribers = new Map(); this.isSubscribing = false; this.chunks = (array, size) => { const result = []; for (let i = 0; i < array.length; i += size) { result.push(array.slice(i, i + size)); } return result; }; this.isSubscribed = false; this.program = program; this.eventEmitter = new events_1.EventEmitter(); this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; this.delistedMarketSetting = delistedMarketSetting; this.resubOpts = resubOpts; this.commitment = commitment; this.skipInitialData = skipInitialData !== null && skipInitialData !== void 0 ? skipInitialData : false; const { rpc, rpcSubscriptions } = (0, gill_1.createSolanaClient)({ urlOrMoniker: this.program.provider.connection.rpcEndpoint, }); this.rpc = rpc; this.rpcSubscriptions = rpcSubscriptions; } async subscribe() { try { const startTime = performance.now(); if (this.isSubscribed) { console.log(`[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() skipped - already subscribed`); return true; } if (this.isSubscribing) { console.log(`[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() waiting for existing subscription`); return await this.subscriptionPromise; } this.isSubscribing = true; // Initialize subscriptionPromiseResolver to a no-op function this.subscriptionPromiseResolver = () => { }; this.subscriptionPromise = new Promise((res) => { this.subscriptionPromiseResolver = res; }); const [perpMarketAccountPubkeys, spotMarketAccountPubkeys] = await Promise.all([ Promise.all(this.perpMarketIndexes.map((marketIndex) => (0, pda_1.getPerpMarketPublicKey)(this.program.programId, marketIndex))), Promise.all(this.spotMarketIndexes.map((marketIndex) => (0, pda_1.getSpotMarketPublicKey)(this.program.programId, marketIndex))), ]); // Profile findAllMarketsAndOracles if needed let findAllMarketsDuration = 0; if (this.shouldFindAllMarketsAndOracles) { const findAllMarketsStartTime = performance.now(); const { perpMarketIndexes, perpMarketAccounts, spotMarketIndexes, spotMarketAccounts, oracleInfos, } = await (0, config_1.findAllMarketAndOracles)(this.program); this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; // front run and set the initial data here to save extra gma call in set initial data this.initialPerpMarketAccountData = new Map(perpMarketAccounts.map((market) => [market.marketIndex, market])); this.initialSpotMarketAccountData = new Map(spotMarketAccounts.map((market) => [market.marketIndex, market])); const findAllMarketsEndTime = performance.now(); findAllMarketsDuration = findAllMarketsEndTime - findAllMarketsStartTime; console.log(`[PROFILING] findAllMarketAndOracles completed in ${findAllMarketsDuration.toFixed(2)}ms (${perpMarketAccounts.length} perp markets, ${spotMarketAccounts.length} spot markets)`); } // Create subscribers this.perpMarketAllAccountsSubscriber = new webSocketProgramAccountsSubscriberV2_1.WebSocketProgramAccountsSubscriberV2('PerpMarketAccountsSubscriber', 'PerpMarket', this.program, this.program.account.perpMarket.coder.accounts.decodeUnchecked.bind(this.program.account.perpMarket.coder.accounts), { filters: [(0, memcmp_1.getPerpMarketAccountsFilter)()], commitment: this.commitment, }, this.resubOpts, perpMarketAccountPubkeys // because we pass these in, it will monitor these accounts and fetch them right away ); this.spotMarketAllAccountsSubscriber = new webSocketProgramAccountsSubscriberV2_1.WebSocketProgramAccountsSubscriberV2('SpotMarketAccountsSubscriber', 'SpotMarket', this.program, this.program.account.spotMarket.coder.accounts.decodeUnchecked.bind(this.program.account.spotMarket.coder.accounts), { filters: [(0, memcmp_1.getSpotMarketAccountsFilter)()], commitment: this.commitment, }, this.resubOpts, spotMarketAccountPubkeys // because we pass these in, it will monitor these accounts and fetch them right away ); // Run all subscriptions in parallel await Promise.all([ // Perp market subscription this.perpMarketAllAccountsSubscriber.subscribe((_accountId, data, context, _buffer) => { if (this.delistedMarketSetting !== types_1.DelistedMarketSetting.Subscribe && (0, types_2.isVariant)(data.status, 'delisted')) { return; } this.perpMarketAccountLatestData.set(data.marketIndex, { data, slot: context.slot, }); this.eventEmitter.emit('perpMarketAccountUpdate', data); this.eventEmitter.emit('update'); }), // Spot market subscription this.spotMarketAllAccountsSubscriber.subscribe((_accountId, data, context, _buffer) => { if (this.delistedMarketSetting !== types_1.DelistedMarketSetting.Subscribe && (0, types_2.isVariant)(data.status, 'delisted')) { return; } this.spotMarketAccountLatestData.set(data.marketIndex, { data, slot: context.slot, }); this.eventEmitter.emit('spotMarketAccountUpdate', data); this.eventEmitter.emit('update'); }), // State account subscription (async () => { const statePublicKey = await (0, pda_1.getDriftStateAccountPublicKey)(this.program.programId); this.stateAccountSubscriber = new webSocketAccountSubscriberV2_1.WebSocketAccountSubscriberV2('state', this.program, statePublicKey, undefined, undefined, this.commitment, this.rpcSubscriptions, this.rpc); await Promise.all([ this.stateAccountSubscriber.fetch(), this.stateAccountSubscriber.subscribe((data) => { this.eventEmitter.emit('stateAccountUpdate', data); this.eventEmitter.emit('update'); }), ]); })(), (async () => { await this.setInitialData(); const subscribeToOraclesStartTime = performance.now(); await this.subscribeToOracles(); const subscribeToOraclesEndTime = performance.now(); const duration = subscribeToOraclesEndTime - subscribeToOraclesStartTime; return duration; })(), ]); // const initialPerpMarketDataFromLatestData = new Map( // Array.from(this.perpMarketAccountLatestData.values()).map((data) => [ // data.data.marketIndex, // data.data, // ]) // ); // const initialSpotMarketDataFromLatestData = new Map( // Array.from(this.spotMarketAccountLatestData.values()).map((data) => [ // data.data.marketIndex, // data.data, // ]) // ); // this.initialPerpMarketAccountData = initialPerpMarketDataFromLatestData; // this.initialSpotMarketAccountData = initialSpotMarketDataFromLatestData; await this.handleDelistedMarketOracles(); await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.eventEmitter.emit('update'); // delete initial data this.removeInitialData(); const totalDuration = performance.now() - startTime; console.log(`[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() completed in ${totalDuration.toFixed(2)}ms`); // Resolve the subscription promise this.isSubscribed = true; this.isSubscribing = false; // Before calling subscriptionPromiseResolver, check if it's defined if (this.subscriptionPromiseResolver) { this.subscriptionPromiseResolver(true); } return true; } catch (error) { console.error('Subscription failed:', error); this.isSubscribing = false; this.subscriptionPromiseResolver(false); return false; } } async fetch() { await this.setInitialData(); } /** * This is a no-op method that always returns true. * Unlike the previous implementation, we don't need to manually subscribe to individual perp markets * because we automatically receive updates for all program account changes via a single websocket subscription. * This means any new perp markets will automatically be included without explicit subscription. * @param marketIndex The perp market index to add (unused) * @returns Promise that resolves to true */ addPerpMarket(_marketIndex) { return Promise.resolve(true); } /** * This is a no-op method that always returns true. * Unlike the previous implementation, we don't need to manually subscribe to individual spot markets * because we automatically receive updates for all program account changes via a single websocket subscription. * This means any new spot markets will automatically be included without explicit subscription. * @param marketIndex The spot market index to add (unused) * @returns Promise that resolves to true */ addSpotMarket(_marketIndex) { return Promise.resolve(true); } // TODO: need more options to skip loading perp market and spot market data. Because of how we fetch within the program account subscribers, I am commenting this all out async setInitialData() { var _a; const connection = this.program.provider.connection; // Profile oracle initial data setup const oracleSetupStartTime = performance.now(); const oracleAccountPubkeyChunks = this.chunks(this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey), 100); const oracleAccountInfos = (await Promise.all(oracleAccountPubkeyChunks.map((oracleAccountPublicKeysChunk) => connection.getMultipleAccountsInfo(oracleAccountPublicKeysChunk)))).flat(); this.initialOraclePriceData = new Map(this.oracleInfos.reduce((result, oracleInfo, i) => { if (!oracleAccountInfos[i]) { return result; } const oracleClient = this.oracleClientCache.get(oracleInfo.source, connection, this.program); const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer(oracleAccountInfos[i].data); result.push([ (0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source), oraclePriceData, ]); return result; }, [])); const oracleSetupEndTime = performance.now(); const oracleSetupDuration = oracleSetupEndTime - oracleSetupStartTime; if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`[PROFILING] Oracle initial data setup completed in ${oracleSetupDuration.toFixed(2)}ms (${this.initialOraclePriceData.size} oracles)`); } // emit initial oracle price data Array.from(this.initialOraclePriceData.entries()).forEach(([oracleId, oraclePriceData]) => { const { publicKey, source } = (0, oracleId_1.getPublicKeyAndSourceFromOracleId)(oracleId); this.eventEmitter.emit('oraclePriceUpdate', publicKey, source, oraclePriceData); }); this.eventEmitter.emit('update'); } removeInitialData() { this.initialPerpMarketAccountData = new Map(); this.initialSpotMarketAccountData = new Map(); this.initialOraclePriceData = new Map(); } async subscribeToOracles() { const startTime = performance.now(); // Filter out default oracles and duplicates to avoid unnecessary subscriptions const validOracleInfos = this.oracleInfos.filter((oracleInfo) => !this.oracleSubscribers.has((0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source))); await Promise.all(validOracleInfos.map((oracleInfo) => this.subscribeToOracle(oracleInfo))); const totalDuration = performance.now() - startTime; console.log(`[PROFILING] subscribeToOracles() completed in ${totalDuration.toFixed(2)}ms`); return true; } async subscribeToOracle(oracleInfo) { var _a; try { const oracleId = (0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source); const client = this.oracleClientCache.get(oracleInfo.source, this.program.provider.connection, this.program); const accountSubscriber = new webSocketAccountSubscriberV2_1.WebSocketAccountSubscriberV2('oracle', this.program, oracleInfo.publicKey, (buffer) => { return client.getOraclePriceDataFromBuffer(buffer); }, this.resubOpts, this.commitment, this.rpcSubscriptions, this.rpc); const initialOraclePriceData = (_a = this.initialOraclePriceData) === null || _a === void 0 ? void 0 : _a.get(oracleId); if (initialOraclePriceData) { accountSubscriber.setData(initialOraclePriceData); } await accountSubscriber.subscribe((data) => { this.eventEmitter.emit('oraclePriceUpdate', oracleInfo.publicKey, oracleInfo.source, data); this.eventEmitter.emit('update'); }); this.oracleSubscribers.set(oracleId, accountSubscriber); return true; } catch (error) { console.error(`Failed to subscribe to oracle ${oracleInfo.publicKey.toString()}:`, error); return false; } } async unsubscribeFromMarketAccounts() { await this.perpMarketAllAccountsSubscriber.unsubscribe(); } async unsubscribeFromSpotMarketAccounts() { await this.spotMarketAllAccountsSubscriber.unsubscribe(); } async unsubscribeFromOracles() { await Promise.all(Array.from(this.oracleSubscribers.values()).map((accountSubscriber) => accountSubscriber.unsubscribe())); } async unsubscribe() { var _a; if (!this.isSubscribed) { return; } if (this.subscriptionPromise) { await this.subscriptionPromise; } await Promise.all([ (_a = this.stateAccountSubscriber) === null || _a === void 0 ? void 0 : _a.unsubscribe(), this.unsubscribeFromMarketAccounts(), this.unsubscribeFromSpotMarketAccounts(), this.unsubscribeFromOracles(), ]); this.isSubscribed = false; this.isSubscribing = false; this.subscriptionPromiseResolver = () => { }; } async addOracle(oracleInfo) { const oracleId = (0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source); if (this.oracleSubscribers.has(oracleId)) { return true; } if (oracleInfo.publicKey.equals(web3_js_1.PublicKey.default)) { return true; } return this.subscribeToOracle(oracleInfo); } async setPerpOracleMap() { const perpMarkets = this.getMarketAccountsAndSlots(); const addOraclePromises = []; for (const perpMarket of perpMarkets) { if (!perpMarket || !perpMarket.data) { continue; } const perpMarketAccount = perpMarket.data; const perpMarketIndex = perpMarketAccount.marketIndex; const oracle = perpMarketAccount.amm.oracle; const oracleId = (0, oracleId_1.getOracleId)(oracle, perpMarket.data.amm.oracleSource); if (!this.oracleSubscribers.has(oracleId)) { addOraclePromises.push(this.addOracle({ publicKey: oracle, source: perpMarket.data.amm.oracleSource, })); } this.perpOracleMap.set(perpMarketIndex, oracle); this.perpOracleStringMap.set(perpMarketIndex, oracleId); } await Promise.all(addOraclePromises); } async setSpotOracleMap() { const spotMarkets = this.getSpotMarketAccountsAndSlots(); const addOraclePromises = []; for (const spotMarket of spotMarkets) { if (!spotMarket || !spotMarket.data) { continue; } const spotMarketAccount = spotMarket.data; const spotMarketIndex = spotMarketAccount.marketIndex; const oracle = spotMarketAccount.oracle; const oracleId = (0, oracleId_1.getOracleId)(oracle, spotMarketAccount.oracleSource); if (!this.oracleSubscribers.has(oracleId)) { addOraclePromises.push(this.addOracle({ publicKey: oracle, source: spotMarketAccount.oracleSource, })); } this.spotOracleMap.set(spotMarketIndex, oracle); this.spotOracleStringMap.set(spotMarketIndex, oracleId); } await Promise.all(addOraclePromises); } async handleDelistedMarketOracles() { if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Subscribe) { return; } const { oracles } = (0, utils_1.findDelistedPerpMarketsAndOracles)(this.getMarketAccountsAndSlots(), this.getSpotMarketAccountsAndSlots()); for (const oracle of oracles) { const oracleId = (0, oracleId_1.getOracleId)(oracle.publicKey, oracle.source); if (this.oracleSubscribers.has(oracleId)) { await this.oracleSubscribers.get(oracleId).unsubscribe(); if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Discard) { this.oracleSubscribers.delete(oracleId); } } } } assertIsSubscribed() { if (!this.isSubscribed) { throw new types_1.NotSubscribedError('You must call `subscribe` before using this function'); } } getStateAccountAndSlot() { this.assertIsSubscribed(); return this.stateAccountSubscriber.dataAndSlot; } getMarketAccountAndSlot(marketIndex) { this.assertIsSubscribed(); return this.perpMarketAccountLatestData.get(marketIndex); } getMarketAccountsAndSlots() { return Array.from(this.perpMarketAccountLatestData.values()); } getSpotMarketAccountAndSlot(marketIndex) { this.assertIsSubscribed(); return this.spotMarketAccountLatestData.get(marketIndex); } getSpotMarketAccountsAndSlots() { return Array.from(this.spotMarketAccountLatestData.values()); } getOraclePriceDataAndSlot(oracleId) { var _a; this.assertIsSubscribed(); if (oracleId === ORACLE_DEFAULT_ID) { return { data: quoteAssetOracleClient_1.QUOTE_ORACLE_PRICE_DATA, slot: 0, }; } return (_a = this.oracleSubscribers.get(oracleId)) === null || _a === void 0 ? void 0 : _a.dataAndSlot; } getOraclePriceDataAndSlotForPerpMarket(marketIndex) { const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex); const oracle = this.perpOracleMap.get(marketIndex); const oracleId = this.perpOracleStringMap.get(marketIndex); if (!perpMarketAccount || !oracleId) { return undefined; } if (!perpMarketAccount.data.amm.oracle.equals(oracle)) { // If the oracle has changed, we need to update the oracle map in background this.setPerpOracleMap(); } return this.getOraclePriceDataAndSlot(oracleId); } getOraclePriceDataAndSlotForSpotMarket(marketIndex) { const spotMarketAccount = this.getSpotMarketAccountAndSlot(marketIndex); const oracle = this.spotOracleMap.get(marketIndex); const oracleId = this.spotOracleStringMap.get(marketIndex); if (!spotMarketAccount || !oracleId) { return undefined; } if (!spotMarketAccount.data.oracle.equals(oracle)) { // If the oracle has changed, we need to update the oracle map in background this.setSpotOracleMap(); } return this.getOraclePriceDataAndSlot(oracleId); } } exports.WebSocketDriftClientAccountSubscriberV2 = WebSocketDriftClientAccountSubscriberV2;