UNPKG

@ox-fun/drift-sdk

Version:

SDK for Drift Protocol

357 lines (307 loc) 9.98 kB
import { DriftClientAccountSubscriber, DriftClientAccountEvents, DataAndSlot, } from './types'; import { AccountSubscriber, NotSubscribedError } from './types'; import { SpotMarketAccount, PerpMarketAccount, StateAccount } from '../types'; import { Program } from '@coral-xyz/anchor'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; import { getDriftStateAccountPublicKey, getSpotMarketPublicKey, getPerpMarketPublicKey, } from '../addresses/pda'; import { WebSocketAccountSubscriber } from './webSocketAccountSubscriber'; import { Commitment, PublicKey } from '@solana/web3.js'; import { OracleInfo, OraclePriceData } from '../oracles/types'; import { OracleClientCache } from '../oracles/oracleClientCache'; import * as Buffer from 'buffer'; import { QUOTE_ORACLE_PRICE_DATA } from '../oracles/quoteAssetOracleClient'; import { findAllMarketAndOracles } from '../config'; export class WebSocketDriftClientAccountSubscriber implements DriftClientAccountSubscriber { isSubscribed: boolean; program: Program; commitment?: Commitment; perpMarketIndexes: number[]; spotMarketIndexes: number[]; oracleInfos: OracleInfo[]; oracleClientCache = new OracleClientCache(); resubTimeoutMs?: number; shouldFindAllMarketsAndOracles: boolean; eventEmitter: StrictEventEmitter<EventEmitter, DriftClientAccountEvents>; stateAccountSubscriber?: AccountSubscriber<StateAccount>; perpMarketAccountSubscribers = new Map< number, AccountSubscriber<PerpMarketAccount> >(); spotMarketAccountSubscribers = new Map< number, AccountSubscriber<SpotMarketAccount> >(); oracleSubscribers = new Map<string, AccountSubscriber<OraclePriceData>>(); private isSubscribing = false; private subscriptionPromise: Promise<boolean>; private subscriptionPromiseResolver: (val: boolean) => void; public constructor( program: Program, perpMarketIndexes: number[], spotMarketIndexes: number[], oracleInfos: OracleInfo[], shouldFindAllMarketsAndOracles: boolean, resubTimeoutMs?: number, commitment?: Commitment ) { this.isSubscribed = false; this.program = program; this.eventEmitter = new EventEmitter(); this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; this.resubTimeoutMs = resubTimeoutMs; this.commitment = commitment; } 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, spotMarketIndexes, oracleInfos } = await findAllMarketAndOracles(this.program); this.perpMarketIndexes = perpMarketIndexes; this.spotMarketIndexes = spotMarketIndexes; this.oracleInfos = oracleInfos; } const statePublicKey = await getDriftStateAccountPublicKey( this.program.programId ); // create and activate main state account subscription this.stateAccountSubscriber = new WebSocketAccountSubscriber( 'state', this.program, statePublicKey, undefined, undefined, this.commitment ); await this.stateAccountSubscriber.subscribe((data: StateAccount) => { this.eventEmitter.emit('stateAccountUpdate', data); this.eventEmitter.emit('update'); }); // subscribe to market accounts await this.subscribeToPerpMarketAccounts(); // subscribe to spot market accounts await this.subscribeToSpotMarketAccounts(); // subscribe to oracles await this.subscribeToOracles(); this.eventEmitter.emit('update'); this.isSubscribing = false; this.isSubscribed = true; this.subscriptionPromiseResolver(true); return true; } async subscribeToPerpMarketAccounts(): Promise<boolean> { for (const marketIndex of this.perpMarketIndexes) { await this.subscribeToPerpMarketAccount(marketIndex); } return true; } async subscribeToPerpMarketAccount(marketIndex: number): Promise<boolean> { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, marketIndex ); const accountSubscriber = new WebSocketAccountSubscriber<PerpMarketAccount>( 'perpMarket', this.program, perpMarketPublicKey, undefined, this.resubTimeoutMs, this.commitment ); await accountSubscriber.subscribe((data: PerpMarketAccount) => { this.eventEmitter.emit('perpMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); this.perpMarketAccountSubscribers.set(marketIndex, accountSubscriber); return true; } async subscribeToSpotMarketAccounts(): Promise<boolean> { for (const marketIndex of this.spotMarketIndexes) { await this.subscribeToSpotMarketAccount(marketIndex); } return true; } async subscribeToSpotMarketAccount(marketIndex: number): Promise<boolean> { const marketPublicKey = await getSpotMarketPublicKey( this.program.programId, marketIndex ); const accountSubscriber = new WebSocketAccountSubscriber<SpotMarketAccount>( 'spotMarket', this.program, marketPublicKey, undefined, this.resubTimeoutMs, this.commitment ); await accountSubscriber.subscribe((data: SpotMarketAccount) => { this.eventEmitter.emit('spotMarketAccountUpdate', data); this.eventEmitter.emit('update'); }); this.spotMarketAccountSubscribers.set(marketIndex, accountSubscriber); return true; } async subscribeToOracles(): Promise<boolean> { for (const oracleInfo of this.oracleInfos) { if (!oracleInfo.publicKey.equals(PublicKey.default)) { await this.subscribeToOracle(oracleInfo); } } return true; } async subscribeToOracle(oracleInfo: OracleInfo): Promise<boolean> { const client = this.oracleClientCache.get( oracleInfo.source, this.program.provider.connection ); const accountSubscriber = new WebSocketAccountSubscriber<OraclePriceData>( 'oracle', this.program, oracleInfo.publicKey, (buffer: Buffer) => { return client.getOraclePriceDataFromBuffer(buffer); }, this.resubTimeoutMs, this.commitment ); await accountSubscriber.subscribe((data: OraclePriceData) => { this.eventEmitter.emit('oraclePriceUpdate', oracleInfo.publicKey, data); this.eventEmitter.emit('update'); }); this.oracleSubscribers.set( oracleInfo.publicKey.toString(), accountSubscriber ); return true; } async unsubscribeFromMarketAccounts(): Promise<void> { for (const accountSubscriber of this.perpMarketAccountSubscribers.values()) { await accountSubscriber.unsubscribe(); } } async unsubscribeFromSpotMarketAccounts(): Promise<void> { for (const accountSubscriber of this.spotMarketAccountSubscribers.values()) { await accountSubscriber.unsubscribe(); } } async unsubscribeFromOracles(): Promise<void> { for (const accountSubscriber of this.oracleSubscribers.values()) { await accountSubscriber.unsubscribe(); } } public async fetch(): Promise<void> { 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); } public async unsubscribe(): Promise<void> { if (!this.isSubscribed) { return; } await this.stateAccountSubscriber.unsubscribe(); await this.unsubscribeFromMarketAccounts(); await this.unsubscribeFromSpotMarketAccounts(); await this.unsubscribeFromOracles(); this.isSubscribed = false; } async addSpotMarket(marketIndex: number): Promise<boolean> { if (this.spotMarketAccountSubscribers.has(marketIndex)) { return true; } return this.subscribeToSpotMarketAccount(marketIndex); } async addPerpMarket(marketIndex: number): Promise<boolean> { if (this.perpMarketAccountSubscribers.has(marketIndex)) { return true; } return this.subscribeToPerpMarketAccount(marketIndex); } async addOracle(oracleInfo: OracleInfo): Promise<boolean> { if (this.oracleSubscribers.has(oracleInfo.publicKey.toString())) { return true; } if (oracleInfo.publicKey.equals(PublicKey.default)) { return true; } return this.subscribeToOracle(oracleInfo); } 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 getMarketAccountAndSlot( marketIndex: number ): DataAndSlot<PerpMarketAccount> | undefined { this.assertIsSubscribed(); return this.perpMarketAccountSubscribers.get(marketIndex).dataAndSlot; } public getMarketAccountsAndSlots(): DataAndSlot<PerpMarketAccount>[] { return Array.from(this.perpMarketAccountSubscribers.values()).map( (subscriber) => subscriber.dataAndSlot ); } public getSpotMarketAccountAndSlot( marketIndex: number ): DataAndSlot<SpotMarketAccount> | undefined { this.assertIsSubscribed(); return this.spotMarketAccountSubscribers.get(marketIndex).dataAndSlot; } public getSpotMarketAccountsAndSlots(): DataAndSlot<SpotMarketAccount>[] { return Array.from(this.spotMarketAccountSubscribers.values()).map( (subscriber) => subscriber.dataAndSlot ); } public getOraclePriceDataAndSlot( oraclePublicKey: PublicKey ): DataAndSlot<OraclePriceData> | undefined { this.assertIsSubscribed(); if (oraclePublicKey.equals(PublicKey.default)) { return { data: QUOTE_ORACLE_PRICE_DATA, slot: 0, }; } return this.oracleSubscribers.get(oraclePublicKey.toString()).dataAndSlot; } }