UNPKG

bybit-api-gnome

Version:

Forked for Lick Hunter, Complete & robust node.js SDK for Bybit's REST APIs and WebSockets v5, with TypeScript & integration tests.

404 lines 15.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const events_1 = require("events"); const node_support_1 = require("./util/node-support"); const logger_1 = require("./util/logger"); const requestUtils_1 = require("./util/requestUtils"); const WsStore_1 = __importStar(require("./util/WsStore")); const websocket_util_1 = require("./util/websocket-util"); class WebSocketClientV5 extends events_1.EventEmitter { constructor(options = {}) { super(); this.requestIdCounter = 0; this.pendingRequests = new Map(); this.logger = logger_1.DefaultLogger; this.wsStore = new WsStore_1.default(this.logger); this.options = { pingInterval: 20000, pongTimeout: 10000, reconnectTimeout: 500, restoreSubscriptionsOnReconnect: true, requestTimeoutMs: 10000, ...options, }; } /** * Convert V5WSCategory to WsKey for compatibility with existing WsStore */ getWsKey(category) { switch (category) { case 'spot': return websocket_util_1.WS_KEY_MAP.v5Spot; case 'linear': return websocket_util_1.WS_KEY_MAP.v5Linear; case 'inverse': return websocket_util_1.WS_KEY_MAP.v5Inverse; case 'option': return websocket_util_1.WS_KEY_MAP.v5Option; case 'private': return websocket_util_1.WS_KEY_MAP.v5Private; default: throw new Error(`Unknown V5 WebSocket category: ${category}`); } } /** * Get the WS base URL for different categories */ getWsBaseUrl(category) { const isTestnet = this.options.testnet; const baseUrl = isTestnet ? 'wss://stream-testnet.bybit.com' : 'wss://stream.bybit.com'; return `${baseUrl}/v5/${category}`; } /** * Connect to a WebSocket category */ async connect(category) { const wsUrl = this.getWsBaseUrl(category); const wsKey = this.getWsKey(category); if (this.wsStore.isWsOpen(wsKey)) { this.logger.warning(`WebSocket for ${category} is already connected`); return; } return this.connectWS(category, wsUrl); } /** * Subscribe to topics */ subscribe(category, topics) { const wsKey = this.getWsKey(category); if (!this.wsStore.isWsOpen(wsKey)) { throw new Error(`WebSocket for category ${category} is not connected`); } const request = { op: 'subscribe', args: topics, req_id: this.generateRequestId(), }; this.sendWSMessage(category, request); // Store subscribed topics topics.forEach(topic => { this.wsStore.addTopic(wsKey, topic); }); } /** * Unsubscribe from topics */ unsubscribe(category, topics) { const wsKey = this.getWsKey(category); if (!this.wsStore.isWsOpen(wsKey)) { throw new Error(`WebSocket for category ${category} is not connected`); } const request = { op: 'unsubscribe', args: topics, req_id: this.generateRequestId(), }; this.sendWSMessage(category, request); // Remove topics from store topics.forEach(topic => { this.wsStore.deleteTopic(wsKey, topic); }); } /** * Subscribe to public market data topics */ subscribePublicTopic(category, topic) { this.subscribe(category, [topic]); } /** * Subscribe to private topics (requires authentication) */ subscribePrivateTopic(topics) { if (!this.options.key || !this.options.secret) { throw new Error('API key and secret are required for private topics'); } this.subscribe('private', topics); } /** * Close connection for a specific category */ close(category) { const wsKey = this.getWsKey(category); const ws = this.wsStore.getWs(wsKey); if (ws) { this.wsStore.setConnectionState(wsKey, WsStore_1.WsConnectionStateEnum.CLOSING); ws.close(); } } /** * Close all connections */ closeAll() { const wsKeys = this.wsStore.getKeys(); // Convert back to categories for the close method const v5Categories = []; wsKeys.forEach(key => { if (key === websocket_util_1.WS_KEY_MAP.v5Spot) v5Categories.push('spot'); else if (key === websocket_util_1.WS_KEY_MAP.v5Linear) v5Categories.push('linear'); else if (key === websocket_util_1.WS_KEY_MAP.v5Inverse) v5Categories.push('inverse'); else if (key === websocket_util_1.WS_KEY_MAP.v5Option) v5Categories.push('option'); else if (key === websocket_util_1.WS_KEY_MAP.v5Private) v5Categories.push('private'); }); v5Categories.forEach(category => this.close(category)); } async connectWS(category, wsUrl) { return new Promise((resolve, reject) => { this.logger.info(`Connecting to ${category}: ${wsUrl}`); const ws = new isomorphic_ws_1.default(wsUrl, undefined, { headers: { 'User-Agent': requestUtils_1.APIID, }, }); const wsKey = this.getWsKey(category); this.wsStore.setWs(wsKey, ws); this.wsStore.setConnectionState(wsKey, WsStore_1.WsConnectionStateEnum.CONNECTING); ws.onopen = () => { this.logger.info(`WebSocket ${category} connected`); this.wsStore.setConnectionState(wsKey, WsStore_1.WsConnectionStateEnum.CONNECTED); this.startPingPong(category); // Authenticate if this is private connection if (category === 'private' && this.options.key && this.options.secret) { this.authenticate(category) .then(() => { this.emit('open', { category }); resolve(); }) .catch(reject); } else { this.emit('open', { category }); resolve(); } }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleMessage(category, message); } catch (error) { this.logger.error(`Failed to parse message for ${category}:`, error); this.emit('error', { category, error }); } }; ws.onerror = (error) => { this.logger.error(`WebSocket ${category} error:`, error); this.emit('error', { category, error }); reject(error); }; ws.onclose = (event) => { this.logger.info(`WebSocket ${category} closed:`, event.code, event.reason); this.wsStore.setConnectionState(wsKey, WsStore_1.WsConnectionStateEnum.INITIAL); this.emit('close', { category, event }); // Attempt reconnection if not manually closed if (event.code !== 1000 && this.options.restoreSubscriptionsOnReconnect) { this.handleReconnection(category); } }; }); } async authenticate(category) { if (!this.options.key || !this.options.secret) { throw new Error('API key and secret are required for authentication'); } const expires = Date.now() + 10000; // 10 seconds from now const signature = await (0, node_support_1.signMessage)(`GET/realtime${expires}`, this.options.secret); const authMessage = { op: 'auth', args: [this.options.key, expires, signature], }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Authentication timeout')); }, this.options.requestTimeoutMs); const handleAuth = (message) => { if (message.op === 'auth') { clearTimeout(timeout); if (message.success) { this.logger.info('WebSocket authentication successful'); resolve(); } else { this.logger.error('WebSocket authentication failed:', message.ret_msg); reject(new Error(`Authentication failed: ${message.ret_msg}`)); } } }; this.once('response', handleAuth); this.sendWSMessage(category, authMessage); }); } handleMessage(category, message) { // Handle pong messages if (message.op === 'pong') { this.logger.debug(`Received pong from ${category}`); return; } // Handle subscription responses if (message.op === 'subscribe' || message.op === 'unsubscribe') { this.emit('response', message); return; } // Handle authentication responses if (message.op === 'auth') { this.emit('response', message); return; } // Handle data updates if (message.topic) { this.emit('update', { category, topic: message.topic, data: message.data, type: message.type, }); return; } // Handle other messages this.emit('message', { category, message }); } sendWSMessage(category, message) { const wsKey = this.getWsKey(category); const ws = this.wsStore.getWs(wsKey); if (!ws || ws.readyState !== isomorphic_ws_1.default.OPEN) { throw new Error(`WebSocket for ${category} is not connected`); } const messageStr = JSON.stringify(message); this.logger.debug(`Sending message to ${category}:`, messageStr); ws.send(messageStr); } startPingPong(category) { const wsKey = this.getWsKey(category); const pingInterval = setInterval(() => { if (!this.wsStore.isWsOpen(wsKey)) { clearInterval(pingInterval); return; } try { this.sendWSMessage(category, { op: 'ping' }); } catch (error) { this.logger.error(`Failed to send ping to ${category}:`, error); clearInterval(pingInterval); } }, this.options.pingInterval); } handleReconnection(category) { const reconnectDelay = this.options.reconnectTimeout; this.logger.info(`Attempting to reconnect ${category} in ${reconnectDelay}ms`); setTimeout(async () => { try { const wsUrl = this.getWsBaseUrl(category); await this.connectWS(category, wsUrl); // Restore subscriptions const wsKey = this.getWsKey(category); const topics = Array.from(this.wsStore.getTopics(wsKey)); if (topics.length > 0) { this.logger.info(`Restoring ${topics.length} subscriptions for ${category}`); this.subscribe(category, topics); } } catch (error) { this.logger.error(`Failed to reconnect ${category}:`, error); // Try again with exponential backoff this.options.reconnectTimeout = Math.min(this.options.reconnectTimeout * 2, 30000); this.handleReconnection(category); } }, reconnectDelay); } generateRequestId() { return `req_${++this.requestIdCounter}_${Date.now()}`; } // Convenience methods for common subscriptions /** * Subscribe to orderbook updates */ subscribeOrderbook(category, symbol, depth = 1) { this.subscribePublicTopic(category, `orderbook.${depth}.${symbol}`); } /** * Subscribe to trade updates */ subscribeTrades(category, symbol) { this.subscribePublicTopic(category, `publicTrade.${symbol}`); } /** * Subscribe to ticker updates */ subscribeTicker(category, symbol) { this.subscribePublicTopic(category, `tickers.${symbol}`); } /** * Subscribe to kline updates */ subscribeKline(category, symbol, interval) { this.subscribePublicTopic(category, `kline.${interval}.${symbol}`); } /** * Subscribe to private order updates */ subscribeOrders() { this.subscribePrivateTopic(['order']); } /** * Subscribe to private position updates */ subscribePositions() { this.subscribePrivateTopic(['position']); } /** * Subscribe to private execution updates */ subscribeExecutions() { this.subscribePrivateTopic(['execution']); } /** * Subscribe to wallet updates */ subscribeWallet() { this.subscribePrivateTopic(['wallet']); } } exports.default = WebSocketClientV5; //# sourceMappingURL=websocket-client-v5.js.map