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