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