UNPKG

@drift-labs/sdk-browser

Version:
456 lines (455 loc) 24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.grpcDriftClientAccountSubscriberV2 = void 0; const events_1 = require("events"); const web3_js_1 = require("@solana/web3.js"); const config_1 = require("../config"); const pda_1 = require("../addresses/pda"); const types_1 = require("./types"); const grpcAccountSubscriber_1 = require("./grpcAccountSubscriber"); const grpcMultiAccountSubscriber_1 = require("./grpcMultiAccountSubscriber"); const oracleId_1 = require("../oracles/oracleId"); const oracleClientCache_1 = require("../oracles/oracleClientCache"); const utils_1 = require("./utils"); class grpcDriftClientAccountSubscriberV2 { constructor(grpcConfigs, program, perpMarketIndexes, spotMarketIndexes, oracleInfos, shouldFindAllMarketsAndOracles, delistedMarketSetting, resubOpts) { this.perpMarketIndexToAccountPubkeyMap = new Map(); this.spotMarketIndexToAccountPubkeyMap = new Map(); this.perpOracleMap = new Map(); this.perpOracleStringMap = new Map(); this.spotOracleMap = new Map(); this.spotOracleStringMap = new Map(); this.oracleIdToOracleDataMap = new Map(); this.oracleClientCache = new oracleClientCache_1.OracleClientCache(); 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.eventEmitter = new events_1.EventEmitter(); this.isSubscribed = false; this.isSubscribing = false; this.program = program; this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; this.oracleInfos = oracleInfos; this.initialPerpMarketAccountData = new Map(); this.initialSpotMarketAccountData = new Map(); this.initialOraclePriceData = new Map(); this.perpOracleMap = new Map(); this.perpOracleStringMap = new Map(); this.spotOracleMap = new Map(); this.spotOracleStringMap = new Map(); this.grpcConfigs = grpcConfigs; this.resubOpts = resubOpts; this.delistedMarketSetting = delistedMarketSetting; } async setInitialData() { const connection = this.program.provider.connection; if (!this.initialPerpMarketAccountData || this.initialPerpMarketAccountData.size === 0) { 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 || this.initialSpotMarketAccountData.size === 0) { 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; }, [])); } async addPerpMarket(_marketIndex) { if (!this.perpMarketIndexes.includes(_marketIndex)) { this.perpMarketIndexes = this.perpMarketIndexes.concat(_marketIndex); } return true; } async addSpotMarket(_marketIndex) { return true; } async addOracle(oracleInfo) { var _a, _c; if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] addOracle'); } if (oracleInfo.publicKey.equals(web3_js_1.PublicKey.default)) { return true; } const exists = this.oracleInfos.some((o) => o.source === oracleInfo.source && o.publicKey.equals(oracleInfo.publicKey)); if (exists) { return true; // Already exists, don't add duplicate } this.oracleInfos = this.oracleInfos.concat(oracleInfo); (_c = this.oracleMultiSubscriber) === null || _c === void 0 ? void 0 : _c.addAccounts([oracleInfo.publicKey]); return true; } 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 = await grpcAccountSubscriber_1.grpcAccountSubscriber.create(this.grpcConfigs, 'state', this.program, statePublicKey, undefined, undefined); 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(); // subscribe to perp + spot markets (separate) and oracles await Promise.all([ this.subscribeToPerpMarketAccounts(), this.subscribeToSpotMarketAccounts(), this.subscribeToOracles(), ]); this.eventEmitter.emit('update'); await this.handleDelistedMarkets(); await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.subscriptionPromiseResolver(true); this.isSubscribing = false; this.isSubscribed = true; // delete initial data this.removeInitialData(); return true; } async fetch() { var _a, _c, _d, _e; await ((_a = this.stateAccountSubscriber) === null || _a === void 0 ? void 0 : _a.fetch()); await ((_c = this.perpMarketsSubscriber) === null || _c === void 0 ? void 0 : _c.fetch()); await ((_d = this.spotMarketsSubscriber) === null || _d === void 0 ? void 0 : _d.fetch()); await ((_e = this.oracleMultiSubscriber) === null || _e === void 0 ? void 0 : _e.fetch()); } assertIsSubscribed() { if (!this.isSubscribed) { throw new types_1.NotSubscribedError('You must call `subscribe` before using this function'); } } getStateAccountAndSlot() { this.assertIsSubscribed(); return this.stateAccountSubscriber.dataAndSlot; } getMarketAccountsAndSlots() { var _a, _c; const map = (_a = this.perpMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountDataMap(); return Array.from((_c = map === null || map === void 0 ? void 0 : map.values()) !== null && _c !== void 0 ? _c : []); } getSpotMarketAccountsAndSlots() { var _a, _c; const map = (_a = this.spotMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountDataMap(); return Array.from((_c = map === null || map === void 0 ? void 0 : map.values()) !== null && _c !== void 0 ? _c : []); } getMarketAccountAndSlot(marketIndex) { var _a; return (_a = this.perpMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountData(this.perpMarketIndexToAccountPubkeyMap.get(marketIndex)); } getSpotMarketAccountAndSlot(marketIndex) { var _a; return (_a = this.spotMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountData(this.spotMarketIndexToAccountPubkeyMap.get(marketIndex)); } getOraclePriceDataAndSlot(oracleId) { this.assertIsSubscribed(); // we need to rely on a map we store in this class because the grpcMultiAccountSubscriber does not track a mapping or oracle ID. // DO NOT call getAccountData on the oracleMultiSubscriber, it will not return the correct data in certain cases(BONK spot and perp market subscribed too at once). return this.oracleIdToOracleDataMap.get(oracleId); } 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); } async setPerpOracleMap() { var _a, _c; const perpMarketsMap = (_a = this.perpMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountDataMap(); const perpMarkets = Array.from(perpMarketsMap.values()); 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 (!((_c = this.oracleMultiSubscriber) === null || _c === void 0 ? void 0 : _c.getAccountDataMap().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() { var _a, _c; const spotMarketsMap = (_a = this.spotMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountDataMap(); const spotMarkets = Array.from(spotMarketsMap.values()); 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 (!((_c = this.oracleMultiSubscriber) === null || _c === void 0 ? void 0 : _c.getAccountDataMap().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 subscribeToPerpMarketAccounts() { var _a; if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] subscribeToPerpMarketAccounts'); } const perpMarketIndexToAccountPubkeys = await Promise.all(this.perpMarketIndexes.map(async (marketIndex) => [ marketIndex, await (0, pda_1.getPerpMarketPublicKey)(this.program.programId, marketIndex), ])); for (const [marketIndex, accountPubkey,] of perpMarketIndexToAccountPubkeys) { this.perpMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); } const perpMarketPubkeys = perpMarketIndexToAccountPubkeys.map(([_, accountPubkey]) => accountPubkey); this.perpMarketsSubscriber = await grpcMultiAccountSubscriber_1.grpcMultiAccountSubscriber.create(this.grpcConfigs, 'perpMarket', this.program, undefined, this.resubOpts, undefined, async () => { var _a; try { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] perp markets subscriber unsubscribed; resubscribing'); } await this.subscribeToPerpMarketAccounts(); } catch (e) { console.error('Perp markets resubscribe failed:', e); } }); for (const data of this.initialPerpMarketAccountData.values()) { this.perpMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); } await this.perpMarketsSubscriber.subscribe(perpMarketPubkeys, (_accountId, data) => { this.eventEmitter.emit('perpMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); return true; } async subscribeToSpotMarketAccounts() { var _a; if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] subscribeToSpotMarketAccounts'); } const spotMarketIndexToAccountPubkeys = await Promise.all(this.spotMarketIndexes.map(async (marketIndex) => [ marketIndex, await (0, pda_1.getSpotMarketPublicKey)(this.program.programId, marketIndex), ])); for (const [marketIndex, accountPubkey,] of spotMarketIndexToAccountPubkeys) { this.spotMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); } const spotMarketPubkeys = spotMarketIndexToAccountPubkeys.map(([_, accountPubkey]) => accountPubkey); this.spotMarketsSubscriber = await grpcMultiAccountSubscriber_1.grpcMultiAccountSubscriber.create(this.grpcConfigs, 'spotMarket', this.program, undefined, this.resubOpts, undefined, async () => { var _a; try { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] spot markets subscriber unsubscribed; resubscribing'); } await this.subscribeToSpotMarketAccounts(); } catch (e) { console.error('Spot markets resubscribe failed:', e); } }); for (const data of this.initialSpotMarketAccountData.values()) { this.spotMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); } await this.spotMarketsSubscriber.subscribe(spotMarketPubkeys, (_accountId, data) => { this.eventEmitter.emit('spotMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); return true; } async subscribeToOracles() { var _a; if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('grpcDriftClientAccountSubscriberV2 subscribeToOracles'); } const oraclePubkeyToInfosMap = new Map(); for (const info of this.oracleInfos) { const pubkey = info.publicKey.toBase58(); if (!oraclePubkeyToInfosMap.has(pubkey)) { oraclePubkeyToInfosMap.set(pubkey, []); } oraclePubkeyToInfosMap.get(pubkey).push(info); } const oraclePubkeys = Array.from(new Set(this.oracleInfos.map((info) => info.publicKey))); this.oracleMultiSubscriber = await grpcMultiAccountSubscriber_1.grpcMultiAccountSubscriber.create(this.grpcConfigs, 'oracle', this.program, (buffer, pubkey, accountProps) => { if (!pubkey) { throw new Error('Oracle pubkey missing in decode'); } const client = this.oracleClientCache.get(accountProps.source, this.program.provider.connection, this.program); const price = client.getOraclePriceDataFromBuffer(buffer); return price; }, this.resubOpts, undefined, async () => { var _a; try { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log('[grpcDriftClientAccountSubscriberV2] oracle subscriber unsubscribed; resubscribing'); } await this.subscribeToOracles(); } catch (e) { console.error('Oracle resubscribe failed:', e); } }, oraclePubkeyToInfosMap); for (const data of this.initialOraclePriceData.entries()) { const { publicKey } = (0, oracleId_1.getPublicKeyAndSourceFromOracleId)(data[0]); this.oracleMultiSubscriber.setAccountData(publicKey.toBase58(), data[1]); this.oracleIdToOracleDataMap.set(data[0], { data: data[1], slot: 0, }); } await this.oracleMultiSubscriber.subscribe(oraclePubkeys, (accountId, data, context, _b, accountProps) => { const oracleId = (0, oracleId_1.getOracleId)(accountId, accountProps.source); this.oracleIdToOracleDataMap.set(oracleId, { data, slot: context.slot, }); this.eventEmitter.emit('oraclePriceUpdate', accountId, accountProps.source, data); this.eventEmitter.emit('update'); }); return true; } async handleDelistedMarkets() { var _a, _c; if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Subscribe) { return; } const { perpMarketIndexes, oracles } = (0, utils_1.findDelistedPerpMarketsAndOracles)(Array.from(((_a = this.perpMarketsSubscriber) === null || _a === void 0 ? void 0 : _a.getAccountDataMap().values()) || []), Array.from(((_c = this.spotMarketsSubscriber) === null || _c === void 0 ? void 0 : _c.getAccountDataMap().values()) || [])); // Build array of perp market pubkeys to remove const perpMarketPubkeysToRemove = perpMarketIndexes .map((marketIndex) => { const pubkeyString = this.perpMarketIndexToAccountPubkeyMap.get(marketIndex); return pubkeyString ? new web3_js_1.PublicKey(pubkeyString) : null; }) .filter((pubkey) => pubkey !== null); // Build array of oracle pubkeys to remove const oraclePubkeysToRemove = oracles.map((oracle) => oracle.publicKey); // Remove accounts in batches - perp markets if (perpMarketPubkeysToRemove.length > 0) { await this.perpMarketsSubscriber.removeAccounts(perpMarketPubkeysToRemove); } // Remove accounts in batches - oracles if (oraclePubkeysToRemove.length > 0) { await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); } } removeInitialData() { this.initialPerpMarketAccountData = new Map(); this.initialSpotMarketAccountData = new Map(); this.initialOraclePriceData = new Map(); } async unsubscribeFromOracles() { if (this.oracleMultiSubscriber) { await this.oracleMultiSubscriber.unsubscribe(); this.oracleMultiSubscriber = undefined; return; } } async unsubscribe() { var _a, _c, _d; if (!this.isSubscribed) { return; } this.isSubscribed = false; this.isSubscribing = false; await ((_a = this.stateAccountSubscriber) === null || _a === void 0 ? void 0 : _a.unsubscribe()); await this.unsubscribeFromOracles(); await ((_c = this.perpMarketsSubscriber) === null || _c === void 0 ? void 0 : _c.unsubscribe()); await ((_d = this.spotMarketsSubscriber) === null || _d === void 0 ? void 0 : _d.unsubscribe()); // Clean up all maps to prevent memory leaks this.perpMarketIndexToAccountPubkeyMap.clear(); this.spotMarketIndexToAccountPubkeyMap.clear(); this.oracleIdToOracleDataMap.clear(); this.perpOracleMap.clear(); this.perpOracleStringMap.clear(); this.spotOracleMap.clear(); this.spotOracleStringMap.clear(); } } exports.grpcDriftClientAccountSubscriberV2 = grpcDriftClientAccountSubscriberV2;