@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
757 lines (677 loc) • 22.6 kB
text/typescript
import StrictEventEmitter from 'strict-event-emitter-types';
import { EventEmitter } from 'events';
import { OracleInfo, OraclePriceData } from '../oracles/types';
import { Program } from '@coral-xyz/anchor';
import { PublicKey } from '@solana/web3.js';
import { findAllMarketAndOracles } from '../config';
import {
getDriftStateAccountPublicKey,
getPerpMarketPublicKey,
getPerpMarketPublicKeySync,
getSpotMarketPublicKey,
getSpotMarketPublicKeySync,
} from '../addresses/pda';
import {
AccountSubscriber,
DataAndSlot,
DelistedMarketSetting,
DriftClientAccountEvents,
DriftClientAccountSubscriber,
NotSubscribedError,
GrpcConfigs,
ResubOpts,
} from './types';
import { grpcAccountSubscriber } from './grpcAccountSubscriber';
import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber';
import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types';
import {
getOracleId,
getPublicKeyAndSourceFromOracleId,
} from '../oracles/oracleId';
import { OracleClientCache } from '../oracles/oracleClientCache';
import { findDelistedPerpMarketsAndOracles } from './utils';
export class grpcDriftClientAccountSubscriberV2
implements DriftClientAccountSubscriber
{
private grpcConfigs: GrpcConfigs;
private perpMarketsSubscriber?: grpcMultiAccountSubscriber<PerpMarketAccount>;
private spotMarketsSubscriber?: grpcMultiAccountSubscriber<SpotMarketAccount>;
private oracleMultiSubscriber?: grpcMultiAccountSubscriber<
OraclePriceData,
OracleInfo
>;
private perpMarketIndexToAccountPubkeyMap = new Map<number, string>();
private spotMarketIndexToAccountPubkeyMap = new Map<number, string>();
private delistedMarketSetting: DelistedMarketSetting;
public eventEmitter: StrictEventEmitter<
EventEmitter,
DriftClientAccountEvents
>;
public isSubscribed: boolean;
public isSubscribing: boolean;
public program: Program;
public perpMarketIndexes: number[];
public spotMarketIndexes: number[];
public shouldFindAllMarketsAndOracles: boolean;
public oracleInfos: OracleInfo[];
public initialPerpMarketAccountData: Map<number, PerpMarketAccount>;
public initialSpotMarketAccountData: Map<number, SpotMarketAccount>;
public initialOraclePriceData: Map<string, OraclePriceData>;
public perpOracleMap = new Map<number, PublicKey>();
public perpOracleStringMap = new Map<number, string>();
public spotOracleMap = new Map<number, PublicKey>();
public spotOracleStringMap = new Map<number, string>();
private oracleIdToOracleDataMap = new Map<
string,
DataAndSlot<OraclePriceData>
>();
public stateAccountSubscriber?: AccountSubscriber<StateAccount>;
oracleClientCache = new OracleClientCache();
private resubOpts?: ResubOpts;
private subscriptionPromise: Promise<boolean>;
protected subscriptionPromiseResolver: (val: boolean) => void;
constructor(
grpcConfigs: GrpcConfigs,
program: Program,
perpMarketIndexes: number[],
spotMarketIndexes: number[],
oracleInfos: OracleInfo[],
shouldFindAllMarketsAndOracles: boolean,
delistedMarketSetting: DelistedMarketSetting,
resubOpts?: ResubOpts
) {
this.eventEmitter = new 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;
}
chunks = <T>(array: readonly T[], size: number): T[][] => {
return new Array(Math.ceil(array.length / size))
.fill(null)
.map((_, index) => index * size)
.map((begin) => array.slice(begin, begin + size));
};
async setInitialData(): Promise<void> {
const connection = this.program.provider.connection;
if (
!this.initialPerpMarketAccountData ||
this.initialPerpMarketAccountData.size === 0
) {
const perpMarketPublicKeys = this.perpMarketIndexes.map((marketIndex) =>
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) =>
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([
getOracleId(oracleInfo.publicKey, oracleInfo.source),
oraclePriceData,
]);
return result;
}, [])
);
}
async addPerpMarket(_marketIndex: number): Promise<boolean> {
if (!this.perpMarketIndexes.includes(_marketIndex)) {
this.perpMarketIndexes = this.perpMarketIndexes.concat(_marketIndex);
}
return true;
}
async addSpotMarket(_marketIndex: number): Promise<boolean> {
return true;
}
async addOracle(oracleInfo: OracleInfo): Promise<boolean> {
if (this.resubOpts?.logResubMessages) {
console.log('[grpcDriftClientAccountSubscriberV2] addOracle');
}
if (oracleInfo.publicKey.equals(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);
this.oracleMultiSubscriber?.addAccounts([oracleInfo.publicKey]);
return true;
}
public async subscribe(): Promise<boolean> {
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 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 getDriftStateAccountPublicKey(
this.program.programId
);
// create and activate main state account subscription
this.stateAccountSubscriber =
await grpcAccountSubscriber.create<StateAccount>(
this.grpcConfigs,
'state',
this.program,
statePublicKey,
undefined,
undefined
);
await this.stateAccountSubscriber.subscribe((data: StateAccount) => {
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;
}
public async fetch(): Promise<void> {
await this.stateAccountSubscriber?.fetch();
await this.perpMarketsSubscriber?.fetch();
await this.spotMarketsSubscriber?.fetch();
await this.oracleMultiSubscriber?.fetch();
}
private assertIsSubscribed(): void {
if (!this.isSubscribed) {
throw new NotSubscribedError(
'You must call `subscribe` before using this function'
);
}
}
public getStateAccountAndSlot(): DataAndSlot<StateAccount> {
this.assertIsSubscribed();
return this.stateAccountSubscriber.dataAndSlot;
}
public getMarketAccountsAndSlots(): DataAndSlot<PerpMarketAccount>[] {
const map = this.perpMarketsSubscriber?.getAccountDataMap();
return Array.from(map?.values() ?? []);
}
public getSpotMarketAccountsAndSlots(): DataAndSlot<SpotMarketAccount>[] {
const map = this.spotMarketsSubscriber?.getAccountDataMap();
return Array.from(map?.values() ?? []);
}
getMarketAccountAndSlot(
marketIndex: number
): DataAndSlot<PerpMarketAccount> | undefined {
return this.perpMarketsSubscriber?.getAccountData(
this.perpMarketIndexToAccountPubkeyMap.get(marketIndex)
);
}
getSpotMarketAccountAndSlot(
marketIndex: number
): DataAndSlot<SpotMarketAccount> | undefined {
return this.spotMarketsSubscriber?.getAccountData(
this.spotMarketIndexToAccountPubkeyMap.get(marketIndex)
);
}
public getOraclePriceDataAndSlot(
oracleId: string
): DataAndSlot<OraclePriceData> | undefined {
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);
}
public getOraclePriceDataAndSlotForPerpMarket(
marketIndex: number
): DataAndSlot<OraclePriceData> | undefined {
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);
}
public getOraclePriceDataAndSlotForSpotMarket(
marketIndex: number
): DataAndSlot<OraclePriceData> | undefined {
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() {
const perpMarketsMap = this.perpMarketsSubscriber?.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 = getOracleId(oracle, perpMarket.data.amm.oracleSource);
if (!this.oracleMultiSubscriber?.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() {
const spotMarketsMap = this.spotMarketsSubscriber?.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 = getOracleId(oracle, spotMarketAccount.oracleSource);
if (!this.oracleMultiSubscriber?.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(): Promise<boolean> {
if (this.resubOpts?.logResubMessages) {
console.log(
'[grpcDriftClientAccountSubscriberV2] subscribeToPerpMarketAccounts'
);
}
const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> =
await Promise.all(
this.perpMarketIndexes.map(async (marketIndex) => [
marketIndex,
await 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.create<PerpMarketAccount>(
this.grpcConfigs,
'perpMarket',
this.program,
undefined,
this.resubOpts,
undefined,
async () => {
try {
if (this.resubOpts?.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 as PerpMarketAccount
);
this.eventEmitter.emit('update');
}
);
return true;
}
async subscribeToSpotMarketAccounts(): Promise<boolean> {
if (this.resubOpts?.logResubMessages) {
console.log(
'[grpcDriftClientAccountSubscriberV2] subscribeToSpotMarketAccounts'
);
}
const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> =
await Promise.all(
this.spotMarketIndexes.map(async (marketIndex) => [
marketIndex,
await 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.create<SpotMarketAccount>(
this.grpcConfigs,
'spotMarket',
this.program,
undefined,
this.resubOpts,
undefined,
async () => {
try {
if (this.resubOpts?.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 as SpotMarketAccount
);
this.eventEmitter.emit('update');
}
);
return true;
}
async subscribeToOracles(): Promise<boolean> {
if (this.resubOpts?.logResubMessages) {
console.log('grpcDriftClientAccountSubscriberV2 subscribeToOracles');
}
const oraclePubkeyToInfosMap = new Map<string, OracleInfo[]>();
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.create<
OraclePriceData,
OracleInfo
>(
this.grpcConfigs,
'oracle',
this.program,
(buffer: Buffer, pubkey?: string, accountProps?: OracleInfo) => {
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 () => {
try {
if (this.resubOpts?.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 } = 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 = 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(): Promise<void> {
if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) {
return;
}
const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles(
Array.from(
this.perpMarketsSubscriber?.getAccountDataMap().values() || []
),
Array.from(this.spotMarketsSubscriber?.getAccountDataMap().values() || [])
);
// Build array of perp market pubkeys to remove
const perpMarketPubkeysToRemove = perpMarketIndexes
.map((marketIndex) => {
const pubkeyString =
this.perpMarketIndexToAccountPubkeyMap.get(marketIndex);
return pubkeyString ? new PublicKey(pubkeyString) : null;
})
.filter((pubkey) => pubkey !== null) as PublicKey[];
// 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(): Promise<void> {
if (this.oracleMultiSubscriber) {
await this.oracleMultiSubscriber.unsubscribe();
this.oracleMultiSubscriber = undefined;
return;
}
}
async unsubscribe(): Promise<void> {
if (!this.isSubscribed) {
return;
}
this.isSubscribed = false;
this.isSubscribing = false;
await this.stateAccountSubscriber?.unsubscribe();
await this.unsubscribeFromOracles();
await this.perpMarketsSubscriber?.unsubscribe();
await this.spotMarketsSubscriber?.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();
}
}