@drift-labs/sdk
Version:
SDK for Drift Protocol
445 lines (444 loc) • 23.3 kB
JavaScript
"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;