UNPKG

bitget-api

Version:

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

404 lines 16.1 kB
import { BaseWebsocketClient, } from './util/BaseWSClient.js'; import { isWsPong } from './util/requestUtils.js'; import { isWSAPIResponse } from './util/type-guards.js'; import { signMessage } from './util/webCryptoAPI.js'; import { getMaxTopicsPerSubscribeEvent, getNormalisedTopicRequests, getPromiseRefForWSAPIRequest, getWsUrl, WS_AUTH_ON_CONNECT_KEYS, WS_KEY_MAP, WS_LOGGER_CATEGORY, } from './util/websocket-util.js'; /** * WebSocket client dedicated to the unified account (V3) WebSockets. * * Your Bitget account needs to be upgraded to unified account mode, to use the account-level WebSocket topics. */ export class WebsocketClientV3 extends BaseWebsocketClient { /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ connectAll() { return [ this.connect(WS_KEY_MAP.v3Private), this.connect(WS_KEY_MAP.v3Public), ]; } /** * Ensures the WS API connection is active and ready. * * You do not need to call this, but if you call this before making any WS API requests, * it can accelerate the first request (by preparing the connection in advance). */ connectWSAPI() { /** This call automatically ensures the connection is active AND authenticated before resolving */ return this.assertIsAuthenticated(WS_KEY_MAP.v3Private); } /** * Request subscription to one or more topics. Pass topics as either an array of strings, * or array of objects (if the topic has parameters). * * Objects should be formatted as {topic: string, params: object, category: CategoryV5}. * * - Subscriptions are automatically routed to the correct websocket connection. * - Authentication/connection is automatic. * - Resubscribe after network issues is automatic. * * Call `unsubscribe(topics)` to remove topics */ subscribe(requests, wsKey) { const topicRequests = Array.isArray(requests) ? requests : [requests]; const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests); return this.subscribeTopicsForWsKey(normalisedTopicRequests, wsKey); } /** * Unsubscribe from one or more topics. Similar to subscribe() but in reverse. * * - Requests are automatically routed to the correct websocket connection. * - These topics will be removed from the topic cache, so they won't be subscribed to again. */ unsubscribe(requests, wsKey) { const topicRequests = Array.isArray(requests) ? requests : [requests]; const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests); return this.unsubscribeTopicsForWsKey(normalisedTopicRequests, wsKey); } /** * * * Internal methods required to integrate with the BaseWSClient * * */ sendPingEvent(wsKey) { this.tryWsSend(wsKey, 'ping'); } sendPongEvent(wsKey) { this.tryWsSend(wsKey, 'pong'); } isWsPing(data) { if (data?.data === 'ping') { return true; } return false; } isWsPong(data) { return isWsPong(data); } isPrivateTopicRequest(_request, wsKey) { return WS_AUTH_ON_CONNECT_KEYS.includes(wsKey); } getPrivateWSKeys() { return WS_AUTH_ON_CONNECT_KEYS; } isAuthOnConnectWsKey(wsKey) { return WS_AUTH_ON_CONNECT_KEYS.includes(wsKey); } async getWsUrl(wsKey) { return getWsUrl(wsKey, this.options, this.logger); } getMaxTopicsPerSubscribeEvent(wsKey) { return getMaxTopicsPerSubscribeEvent(wsKey); } /** * @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec. */ async getWsRequestEvents(operation, requests) { const wsRequestBuildingErrors = []; const topics = requests.map((r) => r.topic + ',' + Object.values(r.payload || {}).join(',')); // Previously used to track topics in a request. Keeping this for subscribe/unsubscribe requests, no need for incremental values const req_id = ['subscribe', 'unsubscribe'].includes(operation) && topics.length ? topics.join(',') : this.getNewRequestId().toFixed(); /** { "op":"subscribe", "args":[ { "instType":"spot", "topic":"ticker", "symbol":"BTCUSDT" }, { "instType":"spot", "topic":"candle5m", "symbol":"BTCUSDT" } ] } */ const wsEvent = { op: operation, args: requests.map((request) => { // const request = { // topic: 'ticker', // payload: { instType: 'spot', symbol: 'BTCUSDT' }, // }; // becomes: // const request = { // topic: 'ticker', // instType: 'spot', // symbol: 'BTCUSDT', // }; return { topic: request.topic, ...request.payload, }; }), }; const midflightWsEvent = { requestKey: req_id, requestEvent: wsEvent, }; if (wsRequestBuildingErrors.length) { const label = wsRequestBuildingErrors.length === requests.length ? 'all' : 'some'; this.logger.error(`Failed to build/send ${wsRequestBuildingErrors.length} event(s) for ${label} WS requests due to exceptions`, { ...WS_LOGGER_CATEGORY, wsRequestBuildingErrors, wsRequestBuildingErrorsStringified: JSON.stringify(wsRequestBuildingErrors, null, 2), }); } return [midflightWsEvent]; } async getWsAuthSignature(wsKey) { const { apiKey, apiSecret, apiPass, recvWindow } = this.options; if (!apiKey || !apiSecret || !apiPass) { this.logger.error('Cannot authenticate websocket, either api key, secret or passphrase missing.', { ...WS_LOGGER_CATEGORY, wsKey }); throw new Error('Cannot auth - missing api or secret or pass in config'); } this.logger.trace("Getting auth'd request params", { ...WS_LOGGER_CATEGORY, wsKey, }); const signatureExpiresAt = ((Date.now() + recvWindow) / 1000).toFixed(0); const signature = await this.signMessage(signatureExpiresAt + 'GET' + '/user/verify', apiSecret, 'base64', 'SHA-256'); return { expiresAt: +signatureExpiresAt, signature, }; } async signMessage(paramsStr, secret, method, algorithm) { if (typeof this.options.customSignMessageFn === 'function') { return this.options.customSignMessageFn(paramsStr, secret); } return await signMessage(paramsStr, secret, method, algorithm); } async getWsAuthRequestEvent(wsKey) { try { const { apiKey, apiSecret, apiPass } = this.options; const { signature, expiresAt } = await this.getWsAuthSignature(wsKey); if (!apiKey || !apiSecret || !apiPass) { this.logger.error('Cannot authenticate websocket, either api key, secret or passphrase missing.', { ...WS_LOGGER_CATEGORY, wsKey }); throw new Error('Cannot auth - missing api or secret or pass in config'); } const request = { op: 'login', args: [ { apiKey, passphrase: apiPass, timestamp: expiresAt, sign: signature, }, ], }; return request; } catch (e) { this.logger.error(e, { ...WS_LOGGER_CATEGORY, wsKey }); throw e; } } /** * Abstraction called to sort ws events into emittable event types (response to a request, data update, etc) */ resolveEmittableEvents(wsKey, event) { const results = []; try { const msg = JSON.parse(event.data); const emittableEvent = { ...msg, wsKey }; /** * WS API response handling */ if (isWSAPIResponse(emittableEvent)) { // const eg1 = { // event: 'error', // id: '1', // code: '43012', // msg: 'Insufficient balance', // }; const retCode = emittableEvent.code; const reqId = emittableEvent.id; const isError = retCode !== '0'; const promiseRef = [emittableEvent.id].join('_'); const loggableContext = { wsKey, promiseRef, parsedEvent: emittableEvent, }; if (!reqId) { this.logger.error('WS API response is missing reqId - promisified workflow could get stuck. If this happens, please get in touch with steps to reproduce. Trace:', loggableContext); } if (isError) { try { this.getWsStore().rejectDeferredPromise(wsKey, promiseRef, emittableEvent, true); } catch (e) { this.logger.error('Exception trying to reject WSAPI promise', { ...loggableContext, error: e, }); } results.push({ eventType: 'exception', event: emittableEvent, isWSAPIResponse: true, }); return results; } // WS API Success try { this.getWsStore().resolveDeferredPromise(wsKey, promiseRef, emittableEvent, true); } catch (e) { this.logger.error('Exception trying to resolve WSAPI promise', { ...loggableContext, error: e, }); } results.push({ eventType: 'response', event: emittableEvent, isWSAPIResponse: true, }); return results; } /** * V3 event handling for consumers - behaves the same as V2 */ if (typeof msg === 'object') { if (typeof msg['code'] === 'number') { // v2 authentication event if (msg.event === 'login' && msg.code === 0) { results.push({ eventType: 'response', event: emittableEvent, }); results.push({ eventType: 'authenticated', event: emittableEvent, }); return results; } } if (msg['event']) { results.push({ eventType: 'response', event: emittableEvent, }); if (msg.event === 'error') { this.logger.error('WS Error received', { ...WS_LOGGER_CATEGORY, wsKey, message: msg || 'no message', // messageType: typeof msg, // messageString: JSON.stringify(msg), event, }); results.push({ eventType: 'exception', event: emittableEvent, }); } return results; } if (msg['arg']) { results.push({ eventType: 'update', event: emittableEvent, }); return results; } } this.logger.info('Unhandled/unrecognised ws event message', { ...WS_LOGGER_CATEGORY, message: msg || 'no message', // messageType: typeof msg, // messageString: JSON.stringify(msg), event, wsKey, }); // fallback emit anyway results.push({ eventType: 'update', event: emittableEvent, }); return results; } catch (e) { this.logger.error('Failed to parse ws event message', { ...WS_LOGGER_CATEGORY, error: e, event, wsKey, }); } return results; } /** * V3/UTA supports order placement via WebSockets. This is the WS API: * https://www.bitget.com/api-doc/uta/websocket/private/Place-Order-Channel * * @returns a promise that resolves/rejects when a matching response arrives */ async sendWSAPIRequest(wsKey, operation, category, params, requestFlags) { this.logger.trace(`sendWSAPIRequest(): assert "${wsKey}" is connected`); await this.assertIsConnected(wsKey); this.logger.trace('sendWSAPIRequest()->assertIsConnected() ok'); if (requestFlags?.authIsOptional !== true) { // this.logger.trace('sendWSAPIRequest(): assertIsAuthenticated(${wsKey})...'); await this.assertIsAuthenticated(wsKey); // this.logger.trace('sendWSAPIRequest(): assertIsAuthenticated(${wsKey}) ok'); } const request = { op: 'trade', id: `${this.getNewRequestId()}`, category: category, topic: operation, // Ensure "args" is always wrapped as array args: Array.isArray(params) ? params : [params], }; // Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events const promiseRef = getPromiseRefForWSAPIRequest(request); const deferredPromise = this.getWsStore().createDeferredPromise(wsKey, promiseRef, false); // Enrich returned promise with request context for easier debugging deferredPromise.promise ?.then((res) => { if (!Array.isArray(res)) { res.request = { wsKey, ...request, }; } return res; }) .catch((e) => { if (typeof e === 'string') { this.logger.error('Unexpected string thrown without Error object:', { e, wsKey, request, }); return e; } e.request = { wsKey, operation, params: params, }; // throw e; return e; }); this.logger.trace(`sendWSAPIRequest(): sending raw request: ${JSON.stringify(request, null, 2)}`); // Send event const throwExceptions = false; this.tryWsSend(wsKey, JSON.stringify(request), throwExceptions); this.logger.trace(`sendWSAPIRequest(): sent "${operation}" event with promiseRef(${promiseRef})`); // Return deferred promise, so caller can await this call return deferredPromise.promise; } } //# sourceMappingURL=websocket-client-v3.js.map