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