UNPKG

bitget-api

Version:

Complete Node.js & JavaScript SDK for Bitget V1-V3 REST APIs & WebSockets, with TypeScript & end-to-end tests.

509 lines 22.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebsocketClientLegacyV1 = void 0; /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ const events_1 = require("events"); const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const logger_js_1 = require("./util/logger.js"); const requestUtils_js_1 = require("./util/requestUtils.js"); const webCryptoAPI_js_1 = require("./util/webCryptoAPI.js"); const websocket_util_js_1 = require("./util/websocket-util.js"); const WsStore_js_1 = __importDefault(require("./util/WsStore.js")); const WsStore_types_js_1 = require("./util/WsStore.types.js"); const LOGGER_CATEGORY = { category: 'bitget-ws' }; /** * @deprecated use WebsocketClientV2 instead */ class WebsocketClientLegacyV1 extends events_1.EventEmitter { logger; options; wsStore; constructor(options, logger) { super(); this.logger = logger || logger_js_1.DefaultLogger; this.wsStore = new WsStore_js_1.default(this.logger); this.options = { pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 0, authPrivateConnectionsOnConnect: true, authPrivateRequests: false, ...options, }; } /** * Subscribe to topics & track/persist them. They will be automatically resubscribed to if the connection drops/reconnects. * @param wsTopics topic or list of topics * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ subscribe(wsTopics, isPrivateTopic) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; topics.forEach((topic) => { const wsKey = (0, websocket_util_js_1.getWsKeyForTopicV1)(topic, isPrivateTopic); // Persist this topic to the expected topics list this.wsStore.addTopic(wsKey, topic); // if connected, send subscription request if (this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTED)) { // if not authenticated, dont sub to private topics yet. // This'll happen automatically once authenticated const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated; if (!isAuthenticated) { return this.requestSubscribeTopics(wsKey, topics.filter((topic) => !(0, websocket_util_js_1.isPrivateChannel)(topic.channel))); } return this.requestSubscribeTopics(wsKey, topics); } // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect if (!this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTING) && !this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.RECONNECTING)) { return this.connect(wsKey); } }); } /** * Unsubscribe from topics & remove them from memory. They won't be re-subscribed to if the connection reconnects. * @param wsTopics topic or list of topics * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ unsubscribe(wsTopics, isPrivateTopic) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; topics.forEach((topic) => this.wsStore.deleteTopic((0, websocket_util_js_1.getWsKeyForTopicV1)(topic, isPrivateTopic), topic)); this.wsStore.getKeys().forEach((wsKey) => { // unsubscribe request only necessary if active connection exists if (this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTED)) { this.requestUnsubscribeTopics(wsKey, topics); } }); } /** Get the WsStore that tracks websockets & topics */ getWsStore() { return this.wsStore; } close(wsKey, force) { this.logger.info('Closing connection', { ...LOGGER_CATEGORY, wsKey }); this.setWsState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); const ws = this.getWs(wsKey); ws?.close(); if (force) { (0, websocket_util_js_1.safeTerminateWs)(ws); } } closeAll(force) { this.wsStore.getKeys().forEach((key) => { this.close(key, force); }); } /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ connectAll() { return [this.connect(websocket_util_js_1.WS_KEY_MAP.spotv1), this.connect(websocket_util_js_1.WS_KEY_MAP.mixv1)]; } /** * Request connection to a specific websocket, instead of waiting for automatic connection. */ async connect(wsKey) { try { if (this.wsStore.isWsOpen(wsKey)) { this.logger.error('Refused to connect to ws with existing active connection', { ...LOGGER_CATEGORY, wsKey }); return this.wsStore.getWs(wsKey); } if (this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTING)) { this.logger.error('Refused to connect to ws, connection attempt already active', { ...LOGGER_CATEGORY, wsKey }); return; } if (!this.wsStore.getConnectionState(wsKey) || this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.INITIAL)) { this.setWsState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTING); } const url = this.getWsUrl(wsKey); // + authParams; const ws = this.connectToWsUrl(url, wsKey); return this.wsStore.setWs(wsKey, ws); } catch (err) { this.parseWsError('Connection failed', err, wsKey); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); } } parseWsError(context, error, wsKey) { if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); this.emit('response', { ...error, wsKey }); this.emit('exception', { ...error, wsKey }); return; } switch (error.message) { case 'Unexpected server response: 401': this.logger.error(`${context} due to 401 authorization failure.`, { ...LOGGER_CATEGORY, wsKey, }); break; default: this.logger.error(`${context} due to unexpected response error: "${error?.msg || error?.message || error}"`, { ...LOGGER_CATEGORY, wsKey, error }); break; } this.emit('response', { ...error, wsKey }); this.emit('exception', { ...error, wsKey }); } async getWsAuthSignature(apiKey, apiSecret, apiPass, recvWindow = 0) { if (!apiKey || !apiSecret || !apiPass) { throw new Error('Cannot auth - missing api key, secret or passcode in config'); } const signatureExpiresAt = ((Date.now() + recvWindow) / 1000).toFixed(0); const signature = await (0, webCryptoAPI_js_1.signMessage)(signatureExpiresAt + 'GET' + '/user/verify', apiSecret, 'base64', 'SHA-256'); return { expiresAt: Number(signatureExpiresAt), signature, }; } /** Get a signature, build the auth request and send it */ async sendAuthRequest(wsKey) { try { const { apiKey, apiSecret, apiPass, recvWindow } = this.options; const { signature, expiresAt } = await this.getWsAuthSignature(apiKey, apiSecret, apiPass, recvWindow); this.logger.info('Sending auth request...', { ...LOGGER_CATEGORY, wsKey, }); const request = { op: 'login', args: [ { apiKey: this.options.apiKey, passphrase: this.options.apiPass, timestamp: expiresAt, sign: signature, }, ], }; // console.log('ws auth req', request); return this.tryWsSend(wsKey, JSON.stringify(request)); } catch (e) { this.logger.trace(e, { ...LOGGER_CATEGORY, wsKey }); } } reconnectWithDelay(wsKey, connectionDelayMs) { this.clearTimers(wsKey); if (this.wsStore.getConnectionState(wsKey) !== WsStore_types_js_1.WsConnectionStateEnum.CONNECTING) { this.setWsState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.RECONNECTING); } this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.logger.info('Reconnecting to websocket', { ...LOGGER_CATEGORY, wsKey, }); this.connect(wsKey); }, connectionDelayMs); } ping(wsKey) { if (this.wsStore.get(wsKey, true).activePongTimer) { return; } this.clearPongTimer(wsKey); this.logger.trace('Sending ping', { ...LOGGER_CATEGORY, wsKey }); this.tryWsSend(wsKey, 'ping'); this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { this.logger.info('Pong timeout - closing socket to reconnect', { ...LOGGER_CATEGORY, wsKey, }); (0, websocket_util_js_1.safeTerminateWs)(this.getWs(wsKey), true); delete this.wsStore.get(wsKey, true).activePongTimer; }, this.options.pongTimeout); } clearTimers(wsKey) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); const wsState = this.wsStore.get(wsKey); if (wsState?.activeReconnectTimer) { clearTimeout(wsState.activeReconnectTimer); } } // Send a ping at intervals clearPingTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState?.activePingTimer) { clearInterval(wsState.activePingTimer); wsState.activePingTimer = undefined; } } // Expect a pong within a time limit clearPongTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState?.activePongTimer) { clearTimeout(wsState.activePongTimer); wsState.activePongTimer = undefined; } } /** * @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics. */ requestSubscribeTopics(wsKey, topics) { if (!topics.length) { return; } const maxTopicsPerEvent = (0, websocket_util_js_1.getMaxTopicsPerSubscribeEvent)(wsKey); if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) { this.logger.trace(`Subscribing to topics in batches of ${maxTopicsPerEvent}`); for (let i = 0; i < topics.length; i += maxTopicsPerEvent) { const batch = topics.slice(i, i + maxTopicsPerEvent); this.logger.trace(`Subscribing to batch of ${batch.length}`); this.requestSubscribeTopics(wsKey, batch); } this.logger.trace(`Finished batch subscribing to ${topics.length} topics`); return; } const wsMessage = JSON.stringify({ op: 'subscribe', args: topics, }); this.tryWsSend(wsKey, wsMessage); } /** * @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics. */ requestUnsubscribeTopics(wsKey, topics) { if (!topics.length) { return; } const maxTopicsPerEvent = (0, websocket_util_js_1.getMaxTopicsPerSubscribeEvent)(wsKey); if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) { this.logger.trace(`Unsubscribing to topics in batches of ${maxTopicsPerEvent}`); for (let i = 0; i < topics.length; i += maxTopicsPerEvent) { const batch = topics.slice(i, i + maxTopicsPerEvent); this.logger.trace(`Unsubscribing to batch of ${batch.length}`); this.requestUnsubscribeTopics(wsKey, batch); } this.logger.trace(`Finished batch unsubscribing to ${topics.length} topics`); return; } const wsMessage = JSON.stringify({ op: 'unsubscribe', args: topics, }); this.tryWsSend(wsKey, wsMessage); } tryWsSend(wsKey, wsMessage) { try { this.logger.trace('Sending upstream ws message: ', { ...LOGGER_CATEGORY, wsMessage, wsKey, }); if (!wsKey) { throw new Error('Cannot send message due to no known websocket for this wsKey'); } const ws = this.getWs(wsKey); if (!ws) { throw new Error(`${wsKey} socket not connected yet, call "connectAll()" first then try again when the "open" event arrives`); } ws.send(wsMessage); } catch (e) { this.logger.error('Failed to send WS message', { ...LOGGER_CATEGORY, wsMessage, wsKey, exception: e, }); } } connectToWsUrl(url, wsKey) { this.logger.trace(`Opening WS connection to URL: ${url}`, { ...LOGGER_CATEGORY, wsKey, }); const { protocols = [], ...wsOptions } = this.options.wsOptions || {}; const ws = new isomorphic_ws_1.default(url, protocols, wsOptions); ws.onopen = (event) => this.onWsOpen(event, wsKey); ws.onmessage = (event) => this.onWsMessage(event, wsKey); ws.onerror = (event) => this.parseWsError('websocket error', event, wsKey); ws.onclose = (event) => this.onWsClose(event, wsKey); return ws; } async onWsOpen(event, wsKey) { if (this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTING)) { this.logger.info('Websocket connected', { ...LOGGER_CATEGORY, wsKey, }); this.emit('open', { wsKey, event }); } else if (this.wsStore.isConnectionState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.RECONNECTING)) { this.logger.info('Websocket reconnected', { ...LOGGER_CATEGORY, wsKey }); this.emit('reconnected', { wsKey, event }); } this.setWsState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.CONNECTED); // Some websockets require an auth packet to be sent after opening the connection if (websocket_util_js_1.WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) { await this.sendAuthRequest(wsKey); } // Reconnect to topics known before it connected // Private topics will be resubscribed to once reconnected const topics = [...this.wsStore.getTopics(wsKey)]; const publicTopics = topics.filter((topic) => !(0, websocket_util_js_1.isPrivateChannel)(topic.channel)); this.requestSubscribeTopics(wsKey, publicTopics); this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval); } /** Handle subscription to private topics _after_ authentication successfully completes asynchronously */ onWsAuthenticated(wsKey) { const wsState = this.wsStore.get(wsKey, true); wsState.isAuthenticated = true; const topics = [...this.wsStore.getTopics(wsKey)]; const privateTopics = topics.filter((topic) => (0, websocket_util_js_1.isPrivateChannel)(topic.channel)); if (privateTopics.length) { this.subscribe(privateTopics, true); } } onWsMessage(event, wsKey) { try { // any message can clear the pong timer - wouldn't get a message if the ws wasn't working this.clearPongTimer(wsKey); if ((0, requestUtils_js_1.isWsPong)(event)) { this.logger.trace('Received pong', { ...LOGGER_CATEGORY, wsKey }); return; } const msg = JSON.parse((event && event['data']) || event); const emittableEvent = { ...msg, wsKey }; if (typeof msg === 'object') { if (typeof msg['code'] === 'number') { if (msg.event === 'login' && msg.code === 0) { this.logger.info('Successfully authenticated WS client', { ...LOGGER_CATEGORY, wsKey, }); this.emit('response', emittableEvent); this.emit('authenticated', emittableEvent); this.onWsAuthenticated(wsKey); return; } } if (msg['event']) { if (msg.event === 'error') { this.logger.error('WS Error received', { ...LOGGER_CATEGORY, wsKey, message: msg || 'no message', // messageType: typeof msg, // messageString: JSON.stringify(msg), event, }); this.emit('exception', emittableEvent); this.emit('response', emittableEvent); return; } return this.emit('response', emittableEvent); } if (msg['arg']) { return this.emit('update', emittableEvent); } } this.logger.info('Unhandled/unrecognised ws event message', { ...LOGGER_CATEGORY, message: msg || 'no message', // messageType: typeof msg, // messageString: JSON.stringify(msg), event, wsKey, }); // fallback emit anyway return this.emit('update', emittableEvent); } catch (e) { this.logger.error('Failed to parse ws event message', { ...LOGGER_CATEGORY, error: e, event, wsKey, }); } } onWsClose(event, wsKey) { this.logger.info('Websocket connection closed', { ...LOGGER_CATEGORY, wsKey, }); if (this.wsStore.getConnectionState(wsKey) !== WsStore_types_js_1.WsConnectionStateEnum.CLOSING) { this.reconnectWithDelay(wsKey, this.options.reconnectTimeout); this.emit('reconnect', { wsKey, event }); } else { this.setWsState(wsKey, WsStore_types_js_1.WsConnectionStateEnum.INITIAL); this.emit('close', { wsKey, event }); } } getWs(wsKey) { return this.wsStore.getWs(wsKey); } setWsState(wsKey, state) { this.wsStore.setConnectionState(wsKey, state); } getWsUrl(wsKey) { if (this.options.wsUrl) { return this.options.wsUrl; } const networkKey = 'livenet'; switch (wsKey) { case websocket_util_js_1.WS_KEY_MAP.spotv1: { return websocket_util_js_1.WS_BASE_URL_MAP.spotv1.all[networkKey]; } case websocket_util_js_1.WS_KEY_MAP.mixv1: { return websocket_util_js_1.WS_BASE_URL_MAP.mixv1.all[networkKey]; } case websocket_util_js_1.WS_KEY_MAP.v2Private: case websocket_util_js_1.WS_KEY_MAP.v2Public: { throw new Error('Use the WebsocketClientV2 for V2 websockets'); } case websocket_util_js_1.WS_KEY_MAP.v3Private: case websocket_util_js_1.WS_KEY_MAP.v3Public: { throw new Error('Use the WebsocketClientV3 for V3 websockets'); } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...LOGGER_CATEGORY, wsKey, }); throw (0, websocket_util_js_1.neverGuard)(wsKey, 'getWsUrl(): Unhandled wsKey'); } } } /** * Subscribe to a topic * @param instType instrument type (refer to API docs). * @param topic topic name (e.g. "ticker"). * @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics. * * @deprecated use WebsocketClientV2 instead */ subscribeTopic(instType, topic, instId = 'default') { return this.subscribe({ instType, instId, channel: topic, }); } /** * Unsubscribe from a topic * @param instType instrument type (refer to API docs). * @param topic topic name (e.g. "ticker"). * @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics to get all symbols. * * @deprecated use WebsocketClientV2 instead */ unsubscribeTopic(instType, topic, instId = 'default') { return this.unsubscribe({ instType, instId, channel: topic, }); } } exports.WebsocketClientLegacyV1 = WebsocketClientLegacyV1; //# sourceMappingURL=websocket-client-legacy-v1.js.map