UNPKG

@drift-labs/sdk

Version:
419 lines (418 loc) • 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PollingDriftClientAccountSubscriber = void 0; const types_1 = require("./types"); const events_1 = require("events"); const types_2 = require("../types"); const pda_1 = require("../addresses/pda"); const utils_1 = require("./utils"); 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 oracleId_1 = require("../oracles/oracleId"); const ORACLE_DEFAULT_ID = (0, oracleId_1.getOracleId)(web3_js_1.PublicKey.default, types_2.OracleSource.QUOTE_ASSET); class PollingDriftClientAccountSubscriber { constructor(program, accountLoader, perpMarketIndexes, spotMarketIndexes, oracleInfos, shouldFindAllMarketsAndOracles, delistedMarketSetting) { this.oracleClientCache = new oracleClientCache_1.OracleClientCache(); this.accountsToPoll = new Map(); this.oraclesToPoll = new Map(); this.perpMarket = new Map(); this.perpOracleMap = new Map(); this.perpOracleStringMap = new Map(); this.spotMarket = new Map(); this.spotOracleMap = new Map(); this.spotOracleStringMap = new Map(); this.oracles = new Map(); this.isSubscribing = false; this.isSubscribed = false; this.program = program; this.eventEmitter = new events_1.EventEmitter(); this.accountLoader = accountLoader; this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; this.delistedMarketSetting = delistedMarketSetting; } 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, spotMarketIndexes, oracleInfos } = await (0, config_1.findAllMarketAndOracles)(this.program); this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; } await this.updateAccountsToPoll(); this.updateOraclesToPoll(); await this.addToAccountLoader(); let subscriptionSucceeded = false; let retries = 0; while (!subscriptionSucceeded && retries < 5) { await this.fetch(); subscriptionSucceeded = this.didSubscriptionSucceed(); retries++; } if (subscriptionSucceeded) { this.eventEmitter.emit('update'); } this.handleDelistedMarkets(); await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); this.isSubscribing = false; this.isSubscribed = subscriptionSucceeded; this.subscriptionPromiseResolver(subscriptionSucceeded); return subscriptionSucceeded; } async updateAccountsToPoll() { if (this.accountsToPoll.size > 0) { return; } const statePublicKey = await (0, pda_1.getDriftStateAccountPublicKey)(this.program.programId); this.accountsToPoll.set(statePublicKey.toString(), { key: 'state', publicKey: statePublicKey, eventType: 'stateAccountUpdate', }); await Promise.all([ this.updatePerpMarketAccountsToPoll(), this.updateSpotMarketAccountsToPoll(), ]); } async updatePerpMarketAccountsToPoll() { await Promise.all(this.perpMarketIndexes.map((marketIndex) => { return this.addPerpMarketAccountToPoll(marketIndex); })); return true; } async addPerpMarketAccountToPoll(marketIndex) { const perpMarketPublicKey = await (0, pda_1.getPerpMarketPublicKey)(this.program.programId, marketIndex); this.accountsToPoll.set(perpMarketPublicKey.toString(), { key: 'perpMarket', publicKey: perpMarketPublicKey, eventType: 'perpMarketAccountUpdate', mapKey: marketIndex, }); return true; } async updateSpotMarketAccountsToPoll() { await Promise.all(this.spotMarketIndexes.map(async (marketIndex) => { await this.addSpotMarketAccountToPoll(marketIndex); })); return true; } async addSpotMarketAccountToPoll(marketIndex) { const marketPublicKey = await (0, pda_1.getSpotMarketPublicKey)(this.program.programId, marketIndex); this.accountsToPoll.set(marketPublicKey.toString(), { key: 'spotMarket', publicKey: marketPublicKey, eventType: 'spotMarketAccountUpdate', mapKey: marketIndex, }); return true; } updateOraclesToPoll() { for (const oracleInfo of this.oracleInfos) { if (!oracleInfo.publicKey.equals(web3_js_1.PublicKey.default)) { this.addOracleToPoll(oracleInfo); } } return true; } addOracleToPoll(oracleInfo) { this.oraclesToPoll.set((0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source), { publicKey: oracleInfo.publicKey, source: oracleInfo.source, }); return true; } async addToAccountLoader() { const accountPromises = []; for (const [_, accountToPoll] of this.accountsToPoll) { accountPromises.push(this.addAccountToAccountLoader(accountToPoll)); } const oraclePromises = []; for (const [_, oracleToPoll] of this.oraclesToPoll) { oraclePromises.push(this.addOracleToAccountLoader(oracleToPoll)); } await Promise.all([...accountPromises, ...oraclePromises]); this.errorCallbackId = this.accountLoader.addErrorCallbacks((error) => { this.eventEmitter.emit('error', error); }); } async addAccountToAccountLoader(accountToPoll) { accountToPoll.callbackId = await this.accountLoader.addAccount(accountToPoll.publicKey, (buffer, slot) => { if (!buffer) return; const account = this.program.account[accountToPoll.key].coder.accounts.decodeUnchecked((0, utils_1.capitalize)(accountToPoll.key), buffer); const dataAndSlot = { data: account, slot, }; if (accountToPoll.mapKey != undefined) { this[accountToPoll.key].set(accountToPoll.mapKey, dataAndSlot); } else { this[accountToPoll.key] = dataAndSlot; } // @ts-ignore this.eventEmitter.emit(accountToPoll.eventType, account); this.eventEmitter.emit('update'); if (!this.isSubscribed) { this.isSubscribed = this.didSubscriptionSucceed(); } }); } async addOracleToAccountLoader(oracleToPoll) { const oracleClient = this.oracleClientCache.get(oracleToPoll.source, this.program.provider.connection, this.program); const oracleId = (0, oracleId_1.getOracleId)(oracleToPoll.publicKey, oracleToPoll.source); oracleToPoll.callbackId = await this.accountLoader.addAccount(oracleToPoll.publicKey, (buffer, slot) => { if (!buffer) return; const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer(buffer); const dataAndSlot = { data: oraclePriceData, slot, }; this.oracles.set(oracleId, dataAndSlot); this.eventEmitter.emit('oraclePriceUpdate', oracleToPoll.publicKey, oracleToPoll.source, oraclePriceData); this.eventEmitter.emit('update'); }); } async fetch() { await this.accountLoader.load(); for (const [_, accountToPoll] of this.accountsToPoll) { const bufferAndSlot = this.accountLoader.getBufferAndSlot(accountToPoll.publicKey); if (!bufferAndSlot) { continue; } const { buffer, slot } = bufferAndSlot; if (buffer) { const account = this.program.account[accountToPoll.key].coder.accounts.decodeUnchecked((0, utils_1.capitalize)(accountToPoll.key), buffer); if (accountToPoll.mapKey != undefined) { this[accountToPoll.key].set(accountToPoll.mapKey, { data: account, slot, }); } else { this[accountToPoll.key] = { data: account, slot, }; } } } for (const [_, oracleToPoll] of this.oraclesToPoll) { const bufferAndSlot = this.accountLoader.getBufferAndSlot(oracleToPoll.publicKey); if (!bufferAndSlot) { continue; } const { buffer, slot } = bufferAndSlot; if (buffer) { const oracleClient = this.oracleClientCache.get(oracleToPoll.source, this.program.provider.connection, this.program); const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer(buffer); this.oracles.set((0, oracleId_1.getOracleId)(oracleToPoll.publicKey, oracleToPoll.source), { data: oraclePriceData, slot, }); } } } didSubscriptionSucceed() { if (this.state) return true; return false; } async unsubscribe() { for (const [_, accountToPoll] of this.accountsToPoll) { this.accountLoader.removeAccount(accountToPoll.publicKey, accountToPoll.callbackId); } for (const [_, oracleToPoll] of this.oraclesToPoll) { this.accountLoader.removeAccount(oracleToPoll.publicKey, oracleToPoll.callbackId); } this.accountLoader.removeErrorCallbacks(this.errorCallbackId); this.errorCallbackId = undefined; this.accountsToPoll.clear(); this.oraclesToPoll.clear(); this.isSubscribed = false; } async addSpotMarket(marketIndex) { const marketPublicKey = await (0, pda_1.getSpotMarketPublicKey)(this.program.programId, marketIndex); if (this.accountsToPoll.has(marketPublicKey.toString())) { return true; } await this.addSpotMarketAccountToPoll(marketIndex); const accountToPoll = this.accountsToPoll.get(marketPublicKey.toString()); await this.addAccountToAccountLoader(accountToPoll); this.setSpotOracleMap(); return true; } async addPerpMarket(marketIndex) { const marketPublicKey = await (0, pda_1.getPerpMarketPublicKey)(this.program.programId, marketIndex); if (this.accountsToPoll.has(marketPublicKey.toString())) { return true; } await this.addPerpMarketAccountToPoll(marketIndex); const accountToPoll = this.accountsToPoll.get(marketPublicKey.toString()); await this.addAccountToAccountLoader(accountToPoll); await this.setPerpOracleMap(); return true; } async addOracle(oracleInfo) { const oracleId = (0, oracleId_1.getOracleId)(oracleInfo.publicKey, oracleInfo.source); if (oracleInfo.publicKey.equals(web3_js_1.PublicKey.default) || this.oracles.has(oracleId)) { return true; } // this func can be called multiple times before the first pauseForOracleToBeAdded finishes // avoid adding to oraclesToPoll multiple time if (!this.oraclesToPoll.has(oracleId)) { this.addOracleToPoll(oracleInfo); const oracleToPoll = this.oraclesToPoll.get(oracleId); await this.addOracleToAccountLoader(oracleToPoll); } await this.pauseForOracleToBeAdded(3, oracleInfo.publicKey.toBase58()); return true; } async pauseForOracleToBeAdded(tries, oracle) { let i = 0; while (i < tries) { await new Promise((r) => setTimeout(r, this.accountLoader.pollingFrequency)); if (this.accountLoader.bufferAndSlotMap.has(oracle)) { return; } i++; } console.log(`Pausing to find oracle ${oracle} failed`); } async setPerpOracleMap() { const perpMarkets = this.getMarketAccountsAndSlots(); const oraclePromises = []; for (const perpMarket of perpMarkets) { const perpMarketAccount = perpMarket.data; const perpMarketIndex = perpMarketAccount.marketIndex; const oracle = perpMarketAccount.amm.oracle; const oracleId = (0, oracleId_1.getOracleId)(oracle, perpMarketAccount.amm.oracleSource); if (!this.oracles.has(oracleId)) { oraclePromises.push(this.addOracle({ publicKey: oracle, source: perpMarketAccount.amm.oracleSource, })); } this.perpOracleMap.set(perpMarketIndex, oracle); this.perpOracleStringMap.set(perpMarketIndex, oracleId); } await Promise.all(oraclePromises); } async setSpotOracleMap() { const spotMarkets = this.getSpotMarketAccountsAndSlots(); const oraclePromises = []; for (const spotMarket of spotMarkets) { const spotMarketAccount = spotMarket.data; const spotMarketIndex = spotMarketAccount.marketIndex; const oracle = spotMarketAccount.oracle; const oracleId = (0, oracleId_1.getOracleId)(oracle, spotMarketAccount.oracleSource); if (!this.oracles.has(oracleId)) { oraclePromises.push(this.addOracle({ publicKey: oracle, source: spotMarketAccount.oracleSource, })); } this.spotOracleMap.set(spotMarketIndex, oracle); this.spotOracleStringMap.set(spotMarketIndex, oracleId); } await Promise.all(oraclePromises); } 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) { const perpMarketPubkey = this.perpMarket.get(perpMarketIndex).data.pubkey; const callbackId = this.accountsToPoll.get(perpMarketPubkey.toBase58()).callbackId; this.accountLoader.removeAccount(perpMarketPubkey, callbackId); if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Discard) { this.perpMarket.delete(perpMarketIndex); } } for (const oracle of oracles) { const oracleId = (0, oracleId_1.getOracleId)(oracle.publicKey, oracle.source); const callbackId = this.oraclesToPoll.get(oracleId).callbackId; this.accountLoader.removeAccount(oracle.publicKey, callbackId); if (this.delistedMarketSetting === types_1.DelistedMarketSetting.Discard) { this.oracles.delete(oracleId); } } } assertIsSubscribed() { if (!this.isSubscribed) { throw new types_1.NotSubscribedError('You must call `subscribe` before using this function'); } } getStateAccountAndSlot() { this.assertIsSubscribed(); return this.state; } getMarketAccountAndSlot(marketIndex) { return this.perpMarket.get(marketIndex); } getMarketAccountsAndSlots() { return Array.from(this.perpMarket.values()); } getSpotMarketAccountAndSlot(marketIndex) { return this.spotMarket.get(marketIndex); } getSpotMarketAccountsAndSlots() { return Array.from(this.spotMarket.values()); } getOraclePriceDataAndSlot(oracleId) { this.assertIsSubscribed(); if (oracleId === ORACLE_DEFAULT_ID) { return { data: quoteAssetOracleClient_1.QUOTE_ORACLE_PRICE_DATA, slot: 0, }; } return this.oracles.get(oracleId); } getOraclePriceDataAndSlotForPerpMarket(marketIndex) { const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex); const oracle = this.perpOracleMap.get(marketIndex); const oracleId = this.perpOracleStringMap.get(marketIndex); if (!perpMarketAccount || !oracle) { 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 || !oracle) { 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); } updateAccountLoaderPollingFrequency(pollingFrequency) { this.accountLoader.updatePollingFrequency(pollingFrequency); } } exports.PollingDriftClientAccountSubscriber = PollingDriftClientAccountSubscriber;