import { PublicKey } from '@solana/web3.js'; import { DataAndSlot, DriftClientAccountEvents, AccountSubscriber, } from './types'; import { StateAccount, PerpMarketAccount, SpotMarketAccount, } from '../types'; import { OracleInfo, OraclePriceData } from '../oracles/types'; import { formatStateAccount, formatPerpMarketAccount, formatSpotMarketAccount, formatOraclePriceData } from './remoteFormat'; import axios, { AxiosInstance} from 'axios'; import { WebSocket as NodeWebSocket } from 'ws'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; import { sleep } from '@switchboard-xyz/on-demand'; const MyWebSocket: any = typeof window !== 'undefined' ? WebSocket : NodeWebSocket; export type MarketInfo = { marketIndex: number; publicKey: PublicKey; }; export type PublicAccountInfo = { statePublicKey: PublicKey; perpMarketInfos: MarketInfo[]; spotMarketInfos: MarketInfo[]; oracleInfos: OracleInfo[]; }; export type PublicData = { channel: string; symbol: string; data: string; } export enum AccountType { STATE = 'state', PERP = 'perpMarket', SPOT = 'spotMarket', ORACLE = 'oracle' } export enum EventType { UPDATE = "update", STATE_ACCOUNT_UPDATE = 'stateAccountUpdate', PERP_MARKET_UPDATE = 'perpMarketAccountUpdate', SPOT_MARKET_UPDATE = 'spotMarketAccountUpdate', ORACLE_PRICE_UPDATE = 'oraclePriceUpdate' } export function log(data: string) { console.log(`${new Date().toLocaleString()} ${data}`); } export default class RemoteUtil { restUrl = 'https://perp-api.openocean.finance'; wsUrl = 'wss://perp-api.openocean.finance/ws/public'; http: AxiosInstance; webSocket: any; httpTimeoutMs = 20000; pingTimeoutId?: NodeJS.Timeout; wsPingTimeoutMs = 60000; connectTimeoutId?: NodeJS.Timeout; wsConnectTimeoutMs = 10000; eventEmitter: StrictEventEmitter; stateAccountSubscriber: AccountSubscriber; perpMarketAccountSubscribers: Map>; spotMarketAccountSubscribers: Map>; oracleSubscribers: Map>; publicAccountInfo: PublicAccountInfo; perpPublicKeyMarketMap = new Map(); spotPublicKeyMarketMap = new Map(); public constructor( eventEmitter: StrictEventEmitter, perpMarketAccountSubscribers: Map>, spotMarketAccountSubscribers: Map>, oracleSubscribers: Map> ) { this.eventEmitter = eventEmitter; this.perpMarketAccountSubscribers = perpMarketAccountSubscribers; this.spotMarketAccountSubscribers = spotMarketAccountSubscribers; this.oracleSubscribers = oracleSubscribers; this.http = axios.create({ baseURL: this.restUrl, timeout: this.httpTimeoutMs, headers: {'content-type': 'application/json;charset=UTF-8'} }); } public setStateAccountSubscriber(stateAccountSubscriber: AccountSubscriber) { this.stateAccountSubscriber = stateAccountSubscriber; } public async getPublicAccountInfo(): Promise { if (!this.publicAccountInfo) { this.publicAccountInfo = await this.httpGet('/drift/v1/public/account'); this.formatPublicAccountInfo(this.publicAccountInfo); this.publicAccountInfo.perpMarketInfos.map(perp => this.perpPublicKeyMarketMap.set(perp.publicKey.toString(), perp.marketIndex)); this.publicAccountInfo.spotMarketInfos.map(spot => this.spotPublicKeyMarketMap.set(spot.publicKey.toString(), spot.marketIndex)); } return this.publicAccountInfo; } public async connect(): Promise { if (this.webSocket) { this.webSocket.close(); } await this.initData(); this.webSocket = new MyWebSocket(this.wsUrl); this.webSocket.onopen = (event) => { log(`connect to websocket server[${this.wsUrl}] success`); if (this.connectTimeoutId) { clearTimeout(this.connectTimeoutId); } this.subscribe(); this.setPingTimeout(); } this.webSocket.onclose = (event) => { log(`websocket closed, code: ${event.code}, reason: ${event.reason}`); } this.webSocket.onerror = (event) => { log(`websocket error, message: ${event.message}`); this.setConnectTimeout(); } this.webSocket.onmessage = (event) => { if (event.data === 'ping') { if (this.pingTimeoutId) { clearTimeout(this.pingTimeoutId); } this.webSocket.send('pong'); this.setPingTimeout(); } else if (event.data) { const publicData = JSON.parse(event.data); if (publicData.code !== undefined && publicData.code !== 0) { log(event.data as string); } else if (publicData.channel) { this.updateData(publicData as PublicData); } } } return true; } private async initData() { const publicDataList: PublicData[] = await this.httpGet('/drift/v1/public/data'); publicDataList.map(data => this.updateData(data)); } private async updateData(publicData: PublicData) { switch (publicData.channel) { case AccountType.STATE: if (this.stateAccountSubscriber) { const dataAndSlot: DataAndSlot = JSON.parse(publicData.data); formatStateAccount(dataAndSlot.data); this.stateAccountSubscriber.setData(dataAndSlot.data, dataAndSlot.slot); this.eventEmitter.emit(EventType.STATE_ACCOUNT_UPDATE, dataAndSlot.data); this.eventEmitter.emit(EventType.UPDATE); } else { log('stateAccountSubscriber not exists'); } break; case AccountType.PERP: const marketIndex = this.perpPublicKeyMarketMap.get(publicData.symbol); if (marketIndex !== undefined) { const accountSubscriber = this.perpMarketAccountSubscribers.get(marketIndex); if (accountSubscriber) { const dataAndSlot: DataAndSlot = JSON.parse(publicData.data); formatPerpMarketAccount(dataAndSlot.data); accountSubscriber.setData(dataAndSlot.data, dataAndSlot.slot); this.eventEmitter.emit(EventType.PERP_MARKET_UPDATE, dataAndSlot.data); this.eventEmitter.emit(EventType.UPDATE); } else { log(`perpMarketAccountSubscriber ${marketIndex} not exists`); } } else { log(`perp publickey:${publicData.symbol} not found marketIndex`); } break; case AccountType.SPOT: const index = this.spotPublicKeyMarketMap.get(publicData.symbol); if (index !== undefined) { const accountSubscriber = this.spotMarketAccountSubscribers.get(index); if (accountSubscriber) { const dataAndSlot: DataAndSlot = JSON.parse(publicData.data); formatSpotMarketAccount(dataAndSlot.data); accountSubscriber.setData(dataAndSlot.data, dataAndSlot.slot); this.eventEmitter.emit(EventType.SPOT_MARKET_UPDATE, dataAndSlot.data); this.eventEmitter.emit(EventType.UPDATE); } else { log(`spotMarketAccountSubscriber ${index} not exists`); } } else { log(`spot publickey:${publicData.symbol} not found marketIndex`); } break; case AccountType.ORACLE: const oracleSubscriber = this.oracleSubscribers.get(publicData.symbol); if (oracleSubscriber) { const dataAndSlot: DataAndSlot = JSON.parse(publicData.data); formatOraclePriceData(dataAndSlot.data); oracleSubscriber.setData(dataAndSlot.data, dataAndSlot.slot); this.eventEmitter.emit(EventType.ORACLE_PRICE_UPDATE, new PublicKey(publicData.symbol), dataAndSlot.data); this.eventEmitter.emit(EventType.UPDATE); } else { log(`oracleSubscriber ${publicData.symbol} not exists`); } break; default: log(`channel ${publicData.channel} not exists`); break; } } private async subscribe() { const args = []; const publicAccountInfo = await this.getPublicAccountInfo(); /** * state */ args.push({ channel: AccountType.STATE, symbol: publicAccountInfo.statePublicKey.toString() }); /** * perp */ publicAccountInfo.perpMarketInfos.map(perp => args.push({ channel: AccountType.PERP, symbol: perp.publicKey.toString() })); /** * spot */ publicAccountInfo.spotMarketInfos.map(spot => args.push({ channel: AccountType.SPOT, symbol: spot.publicKey.toString() })); /** * oracle */ publicAccountInfo.oracleInfos.map(oracle => args.push({ channel: AccountType.ORACLE, symbol: oracle.publicKey.toString() })); /** * stream request */ const streamRequest = { op: 'subscribe', args: args } this.webSocket.send(JSON.stringify(streamRequest)); } private async httpGet(url, config?) { for (let i = 0; i < 3; i++) { try { const response = await this.http.get(url, config); if (response.status === 200) { if (response.data.code === 0) { return response.data.data; } else { log(`http get from ${url} error, code: ${response.data.code}, msg: ${response.data.msg}`); } } else { log(`http get from ${url} error, status: ${response.status}, message: ${response.statusText}`); } } catch (e) { log(`http get from ${url} exception, ${e.toString()}`); } await sleep(this.httpTimeoutMs); } } private setPingTimeout(): void { this.pingTimeoutId = setTimeout( async () => { log('websocket ping timeout, reconnect'); this.connect(); }, this.wsPingTimeoutMs ); } private setConnectTimeout(): void { this.connectTimeoutId = setTimeout( async () => { log('websocket connect timeout, reconnect'); this.connect(); }, this.wsConnectTimeoutMs ); } private formatPublicAccountInfo(accountInfo: PublicAccountInfo) { accountInfo.statePublicKey = new PublicKey(accountInfo.statePublicKey); accountInfo.perpMarketInfos.map(perp => { perp.publicKey = new PublicKey(perp.publicKey); }) accountInfo.spotMarketInfos.map(spot => { spot.publicKey = new PublicKey(spot.publicKey); }) accountInfo.oracleInfos.map(oracle => { oracle.publicKey = new PublicKey(oracle.publicKey); }) } }