UNPKG

@drift-labs/sdk-browser

Version:
372 lines (371 loc) 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketDriftClientAccountSubscriber = void 0; const types_1 = require("./types"); const events_1 = require("events"); const pda_1 = require("../addresses/pda"); const webSocketAccountSubscriber_1 = require("./webSocketAccountSubscriber"); const web3_js_1 = require("@solana/web3.js"); 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_2 = require("../types"); const ORACLE_DEFAULT_ID = (0, oracleId_1.getOracleId)(web3_js_1.PublicKey.default, types_2.OracleSource.QUOTE_ASSET); class WebSocketDriftClientAccountSubscriber { constructor(program, perpMarketIndexes, spotMarketIndexes, oracleInfos, shouldFindAllMarketsAndOracles, delistedMarketSetting, resubOpts, commitment, customPerpMarketAccountSubscriber, customOracleAccountSubscriber) { this.oracleClientCache = new oracleClientCache_1.OracleClientCache(); this.perpMarketAccountSubscribers = new Map(); this.perpOracleMap = new Map(); this.perpOracleStringMap = new Map(); this.spotMarketAccountSubscribers = new Map(); this.spotOracleMap = new Map(); this.spotOracleStringMap = new Map(); this.oracleSubscribers = new Map(); this.isSubscribing = false; this.chunks = (array, size) => { return new Array(Math.ceil(array.length / size)) .fill(null) .map((_, index) => index * size) .map((begin) => array.slice(begin, begin + size)); }; 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.customPerpMarketAccountSubscriber = customPerpMarketAccountSubscriber; this.customOracleAccountSubscriber = customOracleAccountSubscriber; } async subscribe() { if (this.isSubscribed) { return true; } if (this.isSubscribing) { return await this.subscriptionPromise; } this.isSubscribing = true; this.subscriptionPromise = new Promise((res) => { this.subscriptionPromiseResolver = res; }); if (this.shouldFindAllMarketsAndOracles) { 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 statePublicKey = await (0, pda_1.getDriftStateAccountPublicKey)(this.program.programId); // create and activate main state account subscription this.stateAccountSubscriber = new webSocketAccountSubscriber_1.WebSocketAccountSubscriber('state', this.program, statePublicKey, undefined, undefined, this.commitment); await this.stateAccountSubscriber.subscribe((data) => { this.eventEmitter.emit('stateAccountUpdate', data); this.eventEmitter.emit('update'); }); // set initial data to avoid spamming getAccountInfo calls in webSocketAccountSubscriber await this.setInitialData(); await Promise.all([ // subscribe to market accounts this.subscribeToPerpMarketAccounts(), // subscribe to spot market accounts this.subscribeToSpotMarketAccounts(), // subscribe to oracles this.subscribeToOracles(), ]); this.eventEmitter.emit('update'); await this.handleDelistedMarkets(); await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.isSubscribing = false; this.isSubscribed = true; this.subscriptionPromiseResolver(true); // delete initial data this.removeInitialData(); return true; } async setInitialData() { const connection = this.program.provider.connection; if (!this.initialPerpMarketAccountData) { const perpMarketPublicKeys = this.perpMarketIndexes.map((marketIndex) => (0, pda_1.getPerpMarketPublicKeySync)(this.program.programId, marketIndex)); const perpMarketPublicKeysChunks = this.chunks(perpMarketPublicKeys, 75); const perpMarketAccountInfos = (await Promise.all(perpMarketPublicKeysChunks.map((perpMarketPublicKeysChunk) => connection.getMultipleAccountsInfo(perpMarketPublicKeysChunk)))).flat(); this.initialPerpMarketAccountData = new Map(perpMarketAccountInfos .filter((accountInfo) => !!accountInfo) .map((accountInfo) => { const perpMarket = this.program.coder.accounts.decode('PerpMarket', accountInfo.data); return [perpMarket.marketIndex, perpMarket]; })); } if (!this.initialSpotMarketAccountData) { const spotMarketPublicKeys = this.spotMarketIndexes.map((marketIndex) => (0, pda_1.getSpotMarketPublicKeySync)(this.program.programId, marketIndex)); const spotMarketPublicKeysChunks = this.chunks(spotMarketPublicKeys, 75); const spotMarketAccountInfos = (await Promise.all(spotMarketPublicKeysChunks.map((spotMarketPublicKeysChunk) => connection.getMultipleAccountsInfo(spotMarketPublicKeysChunk)))).flat(); this.initialSpotMarketAccountData = new Map(spotMarketAccountInfos .filter((accountInfo) => !!accountInfo) .map((accountInfo) => { const spotMarket = this.program.coder.accounts.decode('SpotMarket', accountInfo.data); return [spotMarket.marketIndex, spotMarket]; })); } const oracleAccountPubkeyChunks = this.chunks(this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey), 75); 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; }, [])); } removeInitialData() { this.initialPerpMarketAccountData = new Map(); this.initialSpotMarketAccountData = new Map(); this.initialOraclePriceData = new Map(); } async subscribeToPerpMarketAccounts() { await Promise.all(this.perpMarketIndexes.map((marketIndex) => this.subscribeToPerpMarketAccount(marketIndex))); return true; } async subscribeToPerpMarketAccount(marketIndex) { const perpMarketPublicKey = await (0, pda_1.getPerpMarketPublicKey)(this.program.programId, marketIndex); const AccountSubscriberClass = this.customPerpMarketAccountSubscriber || webSocketAccountSubscriber_1.WebSocketAccountSubscriber; const accountSubscriber = new AccountSubscriberClass('perpMarket', this.program, perpMarketPublicKey, undefined, this.resubOpts, this.commitment); accountSubscriber.setData(this.initialPerpMarketAccountData.get(marketIndex)); await accountSubscriber.subscribe((data) => { this.eventEmitter.emit('perpMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); this.perpMarketAccountSubscribers.set(marketIndex, accountSubscriber); return true; } async subscribeToSpotMarketAccounts() { await Promise.all(this.spotMarketIndexes.map((marketIndex) => this.subscribeToSpotMarketAccount(marketIndex))); return true; } async subscribeToSpotMarketAccount(marketIndex) { const marketPublicKey = await (0, pda_1.getSpotMarketPublicKey)(this.program.programId, marketIndex); const accountSubscriber = new webSocketAccountSubscriber_1.WebSocketAccountSubscriber('spotMarket', this.program, marketPublicKey, undefined, this.resubOpts, this.commitment); accountSubscriber.setData(this.initialSpotMarketAccountData.get(marketIndex)); await accountSubscriber.subscribe((data) => { this.eventEmitter.emit('spotMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); this.spotMarketAccountSubscribers.set(marketIndex, accountSubscriber); return true; } async subscribeToOracles() { await Promise.all(this.oracleInfos .filter((oracleInfo) => !oracleInfo.publicKey.equals(web3_js_1.PublicKey.default)) .map((oracleInfo) => this.subscribeToOracle(oracleInfo))); return true; } async subscribeToOracle(oracleInfo) { const oracleId = (0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source); const client = this.oracleClientCache.get(oracleInfo.source, this.program.provider.connection, this.program); const AccountSubscriberClass = this.customOracleAccountSubscriber || webSocketAccountSubscriber_1.WebSocketAccountSubscriber; const accountSubscriber = new AccountSubscriberClass('oracle', this.program, oracleInfo.publicKey, (buffer) => { return client.getOraclePriceDataFromBuffer(buffer); }, this.resubOpts, this.commitment); const initialOraclePriceData = this.initialOraclePriceData.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; } async unsubscribeFromMarketAccounts() { await Promise.all(Array.from(this.perpMarketAccountSubscribers.values()).map((accountSubscriber) => accountSubscriber.unsubscribe())); } async unsubscribeFromSpotMarketAccounts() { await Promise.all(Array.from(this.spotMarketAccountSubscribers.values()).map((accountSubscriber) => accountSubscriber.unsubscribe())); } async unsubscribeFromOracles() { await Promise.all(Array.from(this.oracleSubscribers.values()).map((accountSubscriber) => accountSubscriber.unsubscribe())); } async fetch() { if (!this.isSubscribed) { return; } const promises = [this.stateAccountSubscriber.fetch()] .concat(Array.from(this.perpMarketAccountSubscribers.values()).map((subscriber) => subscriber.fetch())) .concat(Array.from(this.spotMarketAccountSubscribers.values()).map((subscriber) => subscriber.fetch())); await Promise.all(promises); } async unsubscribe() { if (!this.isSubscribed) { return; } await this.stateAccountSubscriber.unsubscribe(); await this.unsubscribeFromMarketAccounts(); await this.unsubscribeFromSpotMarketAccounts(); await this.unsubscribeFromOracles(); this.isSubscribed = false; } async addSpotMarket(marketIndex) { if (this.spotMarketAccountSubscribers.has(marketIndex)) { return true; } const subscriptionSuccess = this.subscribeToSpotMarketAccount(marketIndex); await this.setSpotOracleMap(); return subscriptionSuccess; } async addPerpMarket(marketIndex) { if (this.perpMarketAccountSubscribers.has(marketIndex)) { return true; } const subscriptionSuccess = this.subscribeToPerpMarketAccount(marketIndex); await this.setPerpOracleMap(); return subscriptionSuccess; } 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 handleDelistedMarkets() { if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Subscribe) { return; } const { perpMarketIndexes, oracles } = (0, utils_1.findDelistedPerpMarketsAndOracles)(this.getMarketAccountsAndSlots(), this.getSpotMarketAccountsAndSlots()); for (const perpMarketIndex of perpMarketIndexes) { await this.perpMarketAccountSubscribers .get(perpMarketIndex) .unsubscribe(); if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Discard) { this.perpMarketAccountSubscribers.delete(perpMarketIndex); } } for (const oracle of oracles) { const oracleId = (0, oracleId_1.getOracleId)(oracle.publicKey, oracle.source); 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.perpMarketAccountSubscribers.get(marketIndex).dataAndSlot; } getMarketAccountsAndSlots() { return Array.from(this.perpMarketAccountSubscribers.values()).map((subscriber) => subscriber.dataAndSlot); } getSpotMarketAccountAndSlot(marketIndex) { this.assertIsSubscribed(); return this.spotMarketAccountSubscribers.get(marketIndex).dataAndSlot; } getSpotMarketAccountsAndSlots() { return Array.from(this.spotMarketAccountSubscribers.values()).map((subscriber) => subscriber.dataAndSlot); } getOraclePriceDataAndSlot(oracleId) { this.assertIsSubscribed(); if (oracleId === ORACLE_DEFAULT_ID) { return { data: quoteAssetOracleClient_1.QUOTE_ORACLE_PRICE_DATA, slot: 0, }; } return this.oracleSubscribers.get(oracleId).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.WebSocketDriftClientAccountSubscriber = WebSocketDriftClientAccountSubscriber;