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
JavaScript
"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