UNPKG

@hyperfoundation/sdk

Version:
1,069 lines (880 loc) 37.9 kB
import { WebSocketClient } from './connection'; import { AllMids, WsTrade, WsBook, WsBbo, WsOrder, WsUserEvent, Notification, WebData2, Candle, WsUserFills, WsUserFundings, WsUserNonFundingLedgerUpdates, WsUserActiveAssetData, WsActiveSpotAssetCtx, WsActiveAssetCtx, WsTwapHistoryResponse, WsTwapSliceFill, } from '../types/index'; import { SymbolConversion } from '../utils/symbolConversion'; export class WebSocketSubscriptions { private ws: WebSocketClient; private symbolConversion: SymbolConversion; private activeSubscriptions: Map<string, Set<Function>> = new Map(); private subscriptionDetails: Map<string, { type: string; params: any }> = new Map(); constructor(ws: WebSocketClient, symbolConversion: SymbolConversion) { this.ws = ws; this.symbolConversion = symbolConversion; // Listen for reconnect events to resubscribe this.ws.on('reconnect', () => { this.resubscribeAll(); }); } private getSubscriptionKey(type: string, params: any = {}): string { return `${type}:${JSON.stringify(params)}`; } private addSubscriptionCallback(key: string, callback: Function): void { if (!this.activeSubscriptions.has(key)) { this.activeSubscriptions.set(key, new Set()); } this.activeSubscriptions.get(key)?.add(callback); } private removeSubscriptionCallback(key: string, callback: Function): void { const callbacks = this.activeSubscriptions.get(key); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { this.activeSubscriptions.delete(key); } } } private async subscribe(subscription: { type: string; [key: string]: any }): Promise<void> { // Check if we can add another subscription if (!this.ws.incrementSubscriptionCount()) { throw new Error('Maximum subscription limit reached (1000 subscriptions per IP)'); } try { await this.ws.sendMessage({ method: 'subscribe', subscription: subscription }); // Store subscription details for resubscription const subscriptionKey = this.getSubscriptionKey(subscription.type, subscription); this.subscriptionDetails.set(subscriptionKey, { type: subscription.type, params: subscription, }); } catch (error) { // If subscription fails, decrement the count this.ws.decrementSubscriptionCount(); throw error; } } private async unsubscribe(subscription: { type: string; [key: string]: any }): Promise<void> { const convertedSubscription = await this.symbolConversion.convertSymbolsInObject(subscription); await this.ws.sendMessage({ method: 'unsubscribe', subscription: convertedSubscription }); // Decrement subscription count when unsubscribing this.ws.decrementSubscriptionCount(); // Remove subscription details const subscriptionKey = this.getSubscriptionKey(subscription.type, subscription); this.subscriptionDetails.delete(subscriptionKey); } private handleMessage( message: any, callback: (data: any) => void, channel: string, additionalChecks: (data: any) => boolean = () => true ) { if (typeof message !== 'object' || message === null) { console.warn('Received invalid message format:', message); return; } let data = message.data || message; if (data.channel === channel && additionalChecks(data)) { const convertedData = this.symbolConversion.convertSymbolsInObject(data); callback(convertedData); } } async subscribeToAllMids(callback: (data: AllMids) => void): Promise<void> { if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } const subscriptionKey = this.getSubscriptionKey('allMids'); // Remove existing subscription if any if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromAllMids(); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'allMids') { if (message.data.mids) { const convertedData: AllMids = {}; for (const [key, value] of Object.entries(message.data.mids)) { const convertedKey = await this.symbolConversion.convertSymbol(key); const convertedValue = this.symbolConversion.convertToNumber(value); convertedData[convertedKey] = convertedValue; } callback(convertedData); } } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'allMids' }); } async subscribeToNotification( user: string, callback: (data: Notification & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('notification', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromNotification(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'notification') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'notification', user: user }); } async subscribeToWebData2(user: string, callback: (data: WebData2) => void): Promise<void> { const subscriptionKey = this.getSubscriptionKey('webData2', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromWebData2(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'webData2') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'webData2', user: user }); } async subscribeToCandle( coin: string, interval: string, callback: (data: Candle) => void ): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('candle', { coin: convertedCoin, interval }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromCandle(coin, interval); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if ( message.channel === 'candle' && message.data.s === convertedCoin && message.data.i === interval ) { message = await this.symbolConversion.convertSymbolsInObject(message, ['s']); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'candle', coin: convertedCoin, interval: interval }); } async subscribeToL2Book( coin: string, callback: (data: WsBook & { coin: string }) => void ): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('l2Book', { coin: convertedCoin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromL2Book(coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'l2Book' && message.data.coin === convertedCoin) { message = await this.symbolConversion.convertSymbolsInObject(message, ['coin']); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'l2Book', coin: convertedCoin }); } async subscribeToTrades(coin: string, callback: (data: any) => void): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('trades', { coin: convertedCoin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromTrades(coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'trades' && message.data[0].coin === convertedCoin) { message = await this.symbolConversion.convertSymbolsInObject(message, ['coin']); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'trades', coin: convertedCoin }); } async subscribeToOrderUpdates( user: string, callback: (data: WsOrder[] & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('orderUpdates', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromOrderUpdates(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'orderUpdates') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'orderUpdates', user: user }); } async subscribeToUserEvents( user: string, callback: (data: WsUserEvent & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userEvents', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserEvents(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userEvents') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userEvents', user: user }); } async subscribeToUserFills( user: string, callback: (data: WsUserFills & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userFills', { user }); // Remove existing subscription if any if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserFills(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userFills') { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; // Store the message handler with the callback for cleanup (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userFills', user }); } async subscribeToUserFundings( user: string, callback: (data: WsUserFundings & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userFundings', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserFundings(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userFundings') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userFundings', user: user }); } async subscribeToUserNonFundingLedgerUpdates( user: string, callback: (data: WsUserNonFundingLedgerUpdates & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userNonFundingLedgerUpdates', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserNonFundingLedgerUpdates(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userNonFundingLedgerUpdates') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userNonFundingLedgerUpdates', user: user }); } async subscribeToUserActiveAssetData( user: string, coin: string, callback: (data: WsUserActiveAssetData & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('activeAssetData', { user, coin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserActiveAssetData(user, coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'activeAssetData') { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'activeAssetData', user: user, coin: coin }); } /** * Send a POST request via WebSocket * @param requestType - The type of request ('info' or 'action') * @param payload - The payload to send with the request * @param timeout - Optional timeout in milliseconds (default: 30000) * @returns A promise that resolves with the response data */ async postRequest( requestType: 'info' | 'action', payload: any, timeout: number = 30000 ): Promise<any> { // Ensure WebSocket is connected if (!this.ws.isConnected()) { throw new Error('WebSocket is not connected'); } // Generate a unique request ID const id = Date.now() + Math.floor(Math.random() * 1000); console.log(`Preparing WebSocket POST request (ID: ${id}):`, JSON.stringify(payload)); // For WebSocket POST requests, we need to use the exchange format (e.g., 'BTC' instead of 'BTC-PERP') // We need to ensure all coin references are in the exchange format let convertedPayload = { ...payload }; // Helper function to convert a coin to exchange format const convertCoinToExchangeFormat = (coin: string): string => { if (coin && coin.includes('-')) { const parts = coin.split('-'); return parts[0]; // Return just the base symbol (e.g., 'BTC' from 'BTC-PERP') } return coin; }; // Process coin field if it exists if (convertedPayload.coin) { convertedPayload.coin = convertCoinToExchangeFormat(convertedPayload.coin); } // Process arrays of coins if they exist if (Array.isArray(convertedPayload.coins)) { convertedPayload.coins = convertedPayload.coins.map(convertCoinToExchangeFormat); } // For nested objects like in order requests if (convertedPayload.orders) { convertedPayload.orders = convertedPayload.orders.map((order: any) => { if (order.coin) { return { ...order, coin: convertCoinToExchangeFormat(order.coin) }; } return order; }); } // For cancels which might have coin field if (convertedPayload.cancels) { convertedPayload.cancels = convertedPayload.cancels.map((cancel: any) => { if (cancel.coin) { return { ...cancel, coin: convertCoinToExchangeFormat(cancel.coin) }; } return cancel; }); } // For action.cancels which might have coin field if (convertedPayload.action && convertedPayload.action.cancels) { convertedPayload.action.cancels = convertedPayload.action.cancels.map((cancel: any) => { if (cancel.coin) { return { ...cancel, coin: convertCoinToExchangeFormat(cancel.coin) }; } return cancel; }); } // Create the request object according to Hyperliquid API format const request = { method: 'post', id: id, request: { type: requestType, payload: convertedPayload, }, }; console.log(`Sending WebSocket POST request (ID: ${id}):`, JSON.stringify(request)); // Send the request this.ws.sendMessage(request); // Wait for and process the response return new Promise((resolve, reject) => { let receivedMessages = 0; let timeoutId: NodeJS.Timeout; const responseHandler = (message: any) => { // Skip if not an object if (typeof message !== 'object' || message === null) { return; } receivedMessages++; // For debugging - log every 10th message to avoid flooding the console if (receivedMessages % 10 === 0) { console.log( `Received ${receivedMessages} WebSocket messages while waiting for response to request ${id}` ); } // Check if this is a post response if (message.channel === 'post') { console.log(`Received post response:`, JSON.stringify(message)); // Check if this is a response to our request if (message.data && message.data.id === id) { console.log(`Found matching response for request ID ${id}`); // Clean up the event listener and timeout this.ws.removeListener('message', responseHandler); if (timeoutId) { clearTimeout(timeoutId); } // Handle error responses if (message.data.response && message.data.response.type === 'error') { reject(new Error(message.data.response.payload)); return; } try { // Extract and convert the response payload let responseData; if (message.data.response && message.data.response.payload) { responseData = message.data.response.payload; } else if (message.data.response) { responseData = message.data.response; } else { responseData = message.data; } // For the response, we want to convert exchange format back to our internal format // This means adding the '-PERP' suffix to coin symbols const processResponse = (data: any): any => { if (!data || typeof data !== 'object') { return data; } if (Array.isArray(data)) { return data.map(item => processResponse(item)); } const result: any = {}; for (const [key, value] of Object.entries(data)) { if (key === 'coin' && typeof value === 'string') { // Convert coin to internal format (add -PERP suffix if not present) result[key] = value.includes('-') ? value : `${value}-PERP`; } else if (typeof value === 'object' && value !== null) { // Recursively process nested objects result[key] = processResponse(value); } else { // Keep other values as is result[key] = value; } } return result; }; const processedResponse = processResponse(responseData); resolve(processedResponse); } catch (error) { console.error('Error processing response:', error); reject(error); } } } }; // Register the response handler this.ws.on('message', responseHandler); // Set a timeout to prevent hanging requests timeoutId = setTimeout(() => { this.ws.removeListener('message', responseHandler); console.log( `Request ${id} timed out after ${timeout}ms. Received ${receivedMessages} messages.` ); reject(new Error(`WebSocket request timeout after ${timeout}ms`)); }, timeout); }); } async unsubscribeFromAllMids(): Promise<void> { const subscriptionKey = this.getSubscriptionKey('allMids'); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'allMids' }); } async unsubscribeFromNotification(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('notification', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'notification', user: user }); } async unsubscribeFromWebData2(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('webData2', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'webData2', user: user }); } async unsubscribeFromCandle(coin: string, interval: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('candle', { coin: convertedCoin, interval }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'candle', coin: convertedCoin, interval: interval }); } async unsubscribeFromL2Book(coin: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('l2Book', { coin: convertedCoin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'l2Book', coin: convertedCoin }); } async unsubscribeFromTrades(coin: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('trades', { coin: convertedCoin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'trades', coin: convertedCoin }); } async unsubscribeFromOrderUpdates(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('orderUpdates', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'orderUpdates', user: user }); } async unsubscribeFromUserEvents(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userEvents', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userEvents', user: user }); } async unsubscribeFromUserFills(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userFills', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userFills', user }); } async unsubscribeFromUserFundings(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userFundings', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userFundings', user: user }); } async unsubscribeFromUserNonFundingLedgerUpdates(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userNonFundingLedgerUpdates', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userNonFundingLedgerUpdates', user: user }); } async unsubscribeFromUserActiveAssetData(user: string, coin: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('activeAssetData', { user, coin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'activeAssetData', user: user, coin: coin }); } async subscribeToActiveAssetCtx( coin: string, callback: (data: WsActiveAssetCtx) => void ): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('activeAssetCtx', { coin: convertedCoin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromActiveAssetCtx(coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'activeAssetCtx' && message.data.coin === convertedCoin) { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'activeAssetCtx', coin: convertedCoin }); } async subscribeToActiveSpotAssetCtx( coin: string, callback: (data: WsActiveSpotAssetCtx) => void ): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('activeSpotAssetCtx', { coin: convertedCoin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromActiveSpotAssetCtx(coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'activeSpotAssetCtx' && message.data.coin === convertedCoin) { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'activeSpotAssetCtx', coin: convertedCoin }); } async subscribeToUserTwapSliceFills( user: string, callback: (data: WsTwapSliceFill & { user: string }) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userTwapSliceFills', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserTwapSliceFills(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userTwapSliceFills') { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userTwapSliceFills', user }); } async subscribeToBbo(coin: string, callback: (data: WsBbo) => void): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('bbo', { coin: convertedCoin }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromBbo(coin); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'bbo' && message.data.coin === convertedCoin) { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'bbo', coin: convertedCoin }); } async subscribeToUserTwapHistory( user: string, callback: (data: WsTwapHistoryResponse) => void ): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userTwapHistory', { user }); if (this.activeSubscriptions.has(subscriptionKey)) { await this.unsubscribeFromUserTwapHistory(user); } this.addSubscriptionCallback(subscriptionKey, callback); const messageHandler = async (message: any) => { if (message.channel === 'userTwapHistory') { const convertedMessage = await this.symbolConversion.convertSymbolsInObject(message); callback(convertedMessage.data); } }; (callback as any).__messageHandler = messageHandler; this.ws.on('message', messageHandler); await this.subscribe({ type: 'userTwapHistory', user }); } async unsubscribeFromActiveAssetCtx(coin: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('activeAssetCtx', { coin: convertedCoin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'activeAssetCtx', coin: convertedCoin }); } async unsubscribeFromActiveSpotAssetCtx(coin: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('activeSpotAssetCtx', { coin: convertedCoin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'activeSpotAssetCtx', coin: convertedCoin }); } async unsubscribeFromUserTwapSliceFills(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userTwapSliceFills', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userTwapSliceFills', user }); } async unsubscribeFromBbo(coin: string): Promise<void> { const convertedCoin = await this.symbolConversion.convertSymbol(coin, 'reverse'); const subscriptionKey = this.getSubscriptionKey('bbo', { coin: convertedCoin }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'bbo', coin: convertedCoin }); } async unsubscribeFromUserTwapHistory(user: string): Promise<void> { const subscriptionKey = this.getSubscriptionKey('userTwapHistory', { user }); const callbacks = this.activeSubscriptions.get(subscriptionKey); if (callbacks) { for (const callback of callbacks) { const messageHandler = (callback as any).__messageHandler; if (messageHandler) { this.ws.removeListener('message', messageHandler); delete (callback as any).__messageHandler; } } this.activeSubscriptions.delete(subscriptionKey); } await this.unsubscribe({ type: 'userTwapHistory', user }); } /** * Resubscribes to all active subscriptions after a WebSocket reconnection */ async resubscribeAll(): Promise<void> { console.log('Resubscribing to all active subscriptions after reconnection...'); // Reset the subscription count since we're starting fresh after reconnection // The count will be incremented for each subscription as we resubscribe // Create a copy of the subscription details to avoid modification during iteration const subscriptionsToRestore = new Map(this.subscriptionDetails); // Clear the current subscription details as we'll rebuild it this.subscriptionDetails.clear(); // Resubscribe to each subscription for (const [, details] of subscriptionsToRestore.entries()) { try { console.log(`Resubscribing to ${details.type}`); await this.subscribe(details.params); } catch (error) { console.error(`Failed to resubscribe to ${details.type}:`, error); } } console.log('Resubscription complete'); } }