UNPKG

binance

Version:

Professional Node.js & JavaScript SDK for Binance REST APIs & WebSockets, with TypeScript & end-to-end tests.

964 lines (963 loc) 69.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebsocketClient = void 0; const BaseWSClient_1 = require("./util/BaseWSClient"); const beautifier_1 = __importDefault(require("./util/beautifier")); const node_support_1 = require("./util/node-support"); const requestUtils_1 = require("./util/requestUtils"); const typeGuards_1 = require("./util/typeGuards"); const rest_client_cache_1 = require("./util/websockets/rest-client-cache"); const user_data_stream_manager_1 = require("./util/websockets/user-data-stream-manager"); const websocket_util_1 = require("./util/websockets/websocket-util"); const WS_LOGGER_CATEGORY = { category: 'binance-ws' }; /** * Multiplex Node.js, JavaScript & TypeScript Websocket Client for all of Binance's available WebSockets. * * When possible, it will subscribe to all requested topics on a single websocket connection. A list of * all available streams can be seen in the WS_KEY_URL_MAP found in util/websockets/websocket-util.ts. * * Connectivity is automatically maintained. If disconnected, the WebsocketClient will automatically * clean out the old dead connection, respawn a fresh one and resubscribe to all the requested topics. * * If any connection is reconnected, the WS client will: * - Emit the "reconnecting" event when the process begins. * - Emit the "reconnected" event, when the process has completed. When this event arrives, it is often a * good time to execute any synchorisation workflow (e.g. via the REST API) if any information was missed * while disconnected. * * User data streams will use a dedicated connection per stream for increased resilience. */ class WebsocketClient extends BaseWSClient_1.BaseWebsocketClient { constructor(options, logger) { super(options, logger); this.restClientCache = new rest_client_cache_1.RestClientCache(); this.beautifier = new beautifier_1.default({ warnKeyMissingInMap: false, }); this.respawnTimeoutCache = {}; if (options === null || options === void 0 ? void 0 : options.beautifyWarnIfMissing) { this.beautifier.setWarnIfMissing(options.beautifyWarnIfMissing); } /** * Binance uses native WebSocket ping/pong frames, which cannot be directly used in * some environents (e.g. most browsers do not support sending raw ping/pong frames). * * This disables heartbeats in those environments, if ping/pong frames are unavailable. * * Some browsers may still handle these automatically. Some discussion around this can * be found here: https://stackoverflow.com/questions/10585355/sending-websocket-ping-pong-frame-from-browser */ if (!(0, websocket_util_1.isWSPingFrameAvailable)()) { this.logger.info('Disabled WS heartbeats. WS.ping() is not available in your environment.'); this.options.disableHeartbeat = true; } this.userDataStreamManager = new user_data_stream_manager_1.UserDataStreamManager({ logger: this.logger, wsStore: this.getWsStore(), restClientCache: this.restClientCache, // fn pointers: respawnUserDataFn: (wsKey, market, context = {}) => { return this.respawnUserDataStream(wsKey, market, context); }, getWsUrlFn: (wsKey, connectionType) => { return this.getWsUrl(wsKey, connectionType); }, getRestClientOptionsFn: () => this.getRestClientOptions(), getWsClientOptionsfn: () => this.options, closeWsFn: (wsKey, force) => this.close(wsKey, force), connectFn: (wsKey, customUrl, throwOnError) => this.connect(wsKey, customUrl, throwOnError), }); } getUserDataStreamManager() { return this.userDataStreamManager; } getRestClientOptions() { return Object.assign(Object.assign(Object.assign({}, this.options), this.options.restOptions), { testnet: this.options.testnet, api_key: this.options.api_key, api_secret: this.options.api_secret }); } /** * Request connection of all dependent (public & WS API) websockets in prod, instead of waiting * for automatic connection by SDK. * * For the Binance SDK, this will only open public connections (without auth), but is almost definitely overkill if you're only working with one product group. */ connectAll() { return this.connectPublic(); } /** * Request connection to all public websockets in prod (spot, margin, futures, options). Overkill if * you're only working with one product group. */ connectPublic() { return [ this.connect(websocket_util_1.WS_KEY_MAP.main), this.connect(websocket_util_1.WS_KEY_MAP.usdm), this.connect(websocket_util_1.WS_KEY_MAP.coinm), this.connect(websocket_util_1.WS_KEY_MAP.eoptions), ]; } /** * This function serves no purpose in the Binance SDK */ connectPrivate() { return __awaiter(this, void 0, void 0, function* () { return; }); } /** * 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(wsKey, skipAuth) { if (skipAuth) { return this.assertIsConnected(wsKey); } /** This call automatically ensures the connection is active AND authenticated before resolving */ return this.assertIsAuthenticated(wsKey); } /** * 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 = (0, websocket_util_1.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 = (0, websocket_util_1.getNormalisedTopicRequests)(topicRequests); return this.unsubscribeTopicsForWsKey(normalisedTopicRequests, wsKey); } sendWSAPIRequest(wsKey, operation, params, requestFlags) { return __awaiter(this, void 0, void 0, function* () { /** * Spot: * -> https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/general-api-information * -> https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-api.md#public-api-requests * USDM Futures: https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-api-general-info * COINM Futures: https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-api-general-info */ var _a, _b; // If testnet, enforce testnet wskey for WS API calls const resolvedWsKey = this.options.testnet ? (0, websocket_util_1.getTestnetWsKey)(wsKey) : wsKey; // this.logger.trace(`sendWSAPIRequest(): assertIsConnected("${wsKey}")...`); const timestampBeforeAuth = Date.now(); yield this.assertIsConnected(resolvedWsKey); // this.logger.trace('sendWSAPIRequest(): assertIsConnected(${wsKey}) ok'); // Some commands don't require authentication. if ((requestFlags === null || requestFlags === void 0 ? void 0 : requestFlags.authIsOptional) !== true) { // this.logger.trace('sendWSAPIRequest(): assertIsAuthenticated(${wsKey})...'); yield this.assertIsAuthenticated(resolvedWsKey); // this.logger.trace('sendWSAPIRequest(): assertIsAuthenticated(${wsKey}) ok'); } const timestampAfterAuth = Date.now(); const request = { id: this.getNewRequestId(), method: operation, params: Object.assign({}, params), }; /** * Some WS API requests require a timestamp to be included. assertIsConnected and assertIsAuthenticated * can introduce a small delay before the actual request is sent, if not connected before that request is * made. This can lead to a curious race condition, where the request timestamp is before * the "authorizedSince" timestamp - as such, binance does not recognise the session as already authenticated. * * The below mechanism measures any delay introduced from the assert calls, and if the request includes a timestamp, * it offsets that timestamp by the delay. */ const delayFromAuthAssert = timestampAfterAuth - timestampBeforeAuth; if (delayFromAuthAssert && ((_a = request.params) === null || _a === void 0 ? void 0 : _a.timestamp)) { request.params.timestamp += delayFromAuthAssert; this.logger.trace(`sendWSAPIRequest(): adjust timestamp - delay seen by connect/auth assert and delayed request includes timestamp, adjusting timestamp by ${delayFromAuthAssert}ms`); } if ((0, requestUtils_1.requiresWSAPINewClientOID)(request, resolvedWsKey)) { (0, requestUtils_1.validateWSAPINewClientOID)(request, resolvedWsKey); } // Sign, if needed const signedEvent = yield this.signWSAPIRequest(request); // Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events const promiseRef = (0, websocket_util_1.getPromiseRefForWSAPIRequest)(resolvedWsKey, signedEvent); const deferredPromise = this.getWsStore().createDeferredPromise(resolvedWsKey, promiseRef, false); // Enrich returned promise with request context for easier debugging (_b = deferredPromise.promise) === null || _b === void 0 ? void 0 : _b.then((res) => { if (!Array.isArray(res)) { res.request = Object.assign({ wsKey: resolvedWsKey }, signedEvent); } return res; }).catch((e) => { if (typeof e === 'string') { this.logger.error('unexpcted string', { e }); return e; } e.request = { wsKey: resolvedWsKey, operation, params: signedEvent.params, }; // throw e; return e; }); // this.logger.trace( // `sendWSAPIRequest(): sending raw request: ${JSON.stringify(signedEvent)} with promiseRef(${promiseRef})`, // ); // Send event. const throwExceptions = true; this.tryWsSend(resolvedWsKey, JSON.stringify(signedEvent), throwExceptions); this.logger.trace(`sendWSAPIRequest(): sent "${operation}" event with promiseRef(${promiseRef})`); // Return deferred promise, so caller can await this call return deferredPromise.promise; }); } /** * * * Internal methods - not intended for public use * * */ /** * @returns The WS URL to connect to for this WS key */ getWsUrl(wsKey_1) { return __awaiter(this, arguments, void 0, function* (wsKey, connectionType = 'market') { const wsBaseURL = (0, websocket_util_1.getWsUrl)(wsKey, this.options, this.logger) + (0, websocket_util_1.getWsURLSuffix)(wsKey, connectionType); return wsBaseURL; }); } signMessage(paramsStr, secret, method, algorithm) { return __awaiter(this, void 0, void 0, function* () { if (typeof this.options.customSignMessageFn === 'function') { return this.options.customSignMessageFn(paramsStr, secret); } return yield (0, node_support_1.signMessage)(paramsStr, secret, method, algorithm); // return await signMessageWebCryptoAPI(paramsStr, secret, method, algorithm); }); } signWSAPIRequest(requestEvent) { return __awaiter(this, void 0, void 0, function* () { if (!requestEvent.params) { return requestEvent; } /** * Not really needed for most commands on Binance. Their WS API supports * sending signed WS API commands, but that adds latency to every request. * * Instead, this SDK will authenticate once after connecting. All requests * after authentication can then be sent without sign for maximum speed. */ const _a = requestEvent.params, { signRequest } = _a, otherParams = __rest(_a, ["signRequest"]); if (signRequest) { const strictParamValidation = true; const encodeValues = true; const filterUndefinedParams = true; const semiFinalRequestParams = Object.assign({ apiKey: this.options.api_key }, otherParams); const serialisedParams = (0, requestUtils_1.serialiseParams)(semiFinalRequestParams, strictParamValidation, encodeValues, filterUndefinedParams); const signature = yield this.signMessage(serialisedParams, this.options.api_secret, 'base64', 'SHA-256'); return Object.assign(Object.assign({}, requestEvent), { params: Object.assign(Object.assign({}, semiFinalRequestParams), { signature }) }); } return requestEvent; }); } getWsAuthRequestEvent(wsKey) { return __awaiter(this, void 0, void 0, function* () { try { // Note: Only Ed25519 keys are supported for this feature. // If you do not want to specify apiKey and signature in each individual request, you can authenticate your API key for the active WebSocket session. // Once authenticated, you no longer have to specify apiKey and signature for those requests that need them. Requests will be performed on behalf of the account owning the authenticated API key. // Note: You still have to specify the timestamp parameter for SIGNED requests. const timestamp = Date.now() + (this.getTimeOffsetMs() || 0); const strictParamValidation = true; const encodeValues = true; const filterUndefinedParams = true; if (!this.options.api_key || !this.options.api_secret) { throw new Error('API key and/or secret missing, unable to prepare WS auth event without valid API keys.'); } const params = { apiKey: this.options.api_key, timestamp, }; const serialisedParams = (0, requestUtils_1.serialiseParams)(params, strictParamValidation, encodeValues, filterUndefinedParams); const signature = yield this.signMessage(serialisedParams, this.options.api_secret, 'base64', 'SHA-256'); const request = { id: this.getNewRequestId(), method: 'session.logon', params: Object.assign(Object.assign({}, params), { signature }), }; return request; } catch (e) { this.logger.error(e, Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey })); throw e; } }); } sendPingEvent(wsKey) { try { if (!(0, websocket_util_1.isWSPingFrameAvailable)()) { this.logger.trace('Unable to send WS ping frame. Not available in this environment.', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey })); return; } // this.logger.trace(`Sending upstream ping: `, { ...loggerCategory, wsKey }); if (!wsKey) { throw new Error('No wsKey provided'); } const ws = this.getWsStore().getWs(wsKey); if (!ws) { throw new Error(`No active websocket connection exists for wsKey: ${wsKey}`); } // Binance allows unsolicited pongs, so we send both (though we expect a pong in response to our ping if the connection is still alive) if (ws.readyState === 1) { ws.ping(); ws.pong(); } else { this.logger.trace('WS ready state not open - refusing to send WS ping', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey, readyState: ws === null || ws === void 0 ? void 0 : ws.readyState })); } } catch (e) { this.logger.error('Failed to send WS ping', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey, exception: e })); } } sendPongEvent(wsKey) { try { if (!(0, websocket_util_1.isWSPongFrameAvailable)()) { this.logger.trace('Unable to send WS pong frame. Not available in this environment.', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey })); return; } // this.logger.trace(`Sending upstream ping: `, { ...loggerCategory, wsKey }); if (!wsKey) { throw new Error('No wsKey provided'); } const ws = this.getWsStore().getWs(wsKey); if (!ws) { throw new Error(`No active websocket connection exists for wsKey: ${wsKey}`); } // Binance allows unsolicited pongs, so we send both (though we expect a pong in response to our ping if the connection is still alive) if (ws.readyState === 1) { ws.pong(); } else { this.logger.trace('WS ready state not open - refusing to send WS pong', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey, readyState: ws === null || ws === void 0 ? void 0 : ws.readyState })); } } catch (e) { this.logger.error('Failed to send WS pong', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey, exception: e })); } } /** Force subscription requests to be sent in smaller batches, if a number is returned */ getMaxTopicsPerSubscribeEvent(wsKey) { return (0, websocket_util_1.getMaxTopicsPerSubscribeEvent)(wsKey); } /** * @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec. */ getWsRequestEvents(wsKey, operation, requests) { return __awaiter(this, void 0, void 0, function* () { const wsRequestEvents = []; const wsRequestBuildingErrors = []; switch (wsKey) { default: { const topics = requests.map((r) => r.topic); // Previously used to track topics in a request. Keeping this for subscribe/unsubscribe requests, no need for incremental values const req_id = this.getNewRequestId(); const wsEvent = { method: operation, params: topics, id: req_id, }; // Cache midflight subs on the req ID // Enrich response with subs for that req ID const midflightWsEvent = { requestKey: wsEvent.id, requestEvent: wsEvent, }; wsRequestEvents.push(Object.assign({}, midflightWsEvent)); break; } } 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`, Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsRequestBuildingErrors, wsRequestBuildingErrorsStringified: JSON.stringify(wsRequestBuildingErrors, null, 2) })); } return wsRequestEvents; }); } getPrivateWSKeys() { return websocket_util_1.WS_AUTH_ON_CONNECT_KEYS; } isAuthOnConnectWsKey(wsKey) { return websocket_util_1.WS_AUTH_ON_CONNECT_KEYS.includes(wsKey); } /** * Determines if a topic is for a private channel, using a hardcoded list of strings */ isPrivateTopicRequest(request) { var _a; const topicName = (_a = request === null || request === void 0 ? void 0 : request.topic) === null || _a === void 0 ? void 0 : _a.toLowerCase(); if (!topicName) { return false; } return (0, websocket_util_1.isPrivateWsTopic)(topicName); } // eslint-disable-next-line @typescript-eslint/no-explicit-any isWsPing(msg) { if (!msg) { return false; } // For binance, all ping/pong events are frame events return false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any isWsPong(msg) { if (!msg) { return false; } // For binance, all ping/pong events are frame events return false; } /** * Abstraction called to sort ws events into emittable event types (response to a request, data update, etc) */ resolveEmittableEvents(wsKey, event) { var _a, _b; // this.logger.trace(`resolveEmittableEvents(${wsKey}): `, event?.data); const results = []; try { /** * * Extract event from JSON * */ const parsedEvent = (0, websocket_util_1.parseRawWsMessage)(event); /** * * Minor data normalisation & preparation * */ // ws consumers: { data: ... } // ws api consumers (user data): { event: ... } // other responses: { ... } const eventData = (parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.data) || (parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.event) || parsedEvent; const parsedEventId = parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.id; const parsedEventErrorCode = (_a = parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.error) === null || _a === void 0 ? void 0 : _a.code; const streamName = parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.stream; const eventType = // First try, the child node (0, websocket_util_1.parseEventTypeFromMessage)(wsKey, eventData) || // Second try, the parent (0, websocket_util_1.parseEventTypeFromMessage)(wsKey, parsedEvent); // Some events don't include the topic (event name) // This tries to extract and append it, using available context (0, requestUtils_1.appendEventIfMissing)(eventData, wsKey, eventType); const legacyContext = (0, websocket_util_1.getLegacyWsKeyContext)(wsKey); const wsMarket = legacyContext ? legacyContext.market : (0, websocket_util_1.resolveUserDataMarketForWsKey)(wsKey); // This attaches `wsMarket` and `streamName` to incoming events // If the event is an array, it's attached to each element in the array if (Array.isArray(eventData)) { for (const row of eventData) { row.wsMarket = wsMarket; if (streamName && !row.streamName) { row.streamName = streamName; } } } else { eventData.wsMarket = wsMarket; if (streamName && !eventData.streamName) { eventData.streamName = streamName; } } /** * * * Main parsing logic below: * * */ const traceEmittable = false; if (traceEmittable) { this.logger.trace('resolveEmittableEvents', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey, parsedEvent: JSON.stringify(parsedEvent), parsedEventData: JSON.stringify(eventData), eventType, properties: { parsedEventId, parsedEventErrorCode, } })); } const isWSAPIResponse = typeof parsedEventId === 'number'; const EVENTS_AUTHENTICATED = ['auth']; const EVENTS_RESPONSES = [ 'subscribe', 'unsubscribe', 'COMMAND_RESP', 'ping', 'pong', ]; // WS API response if (isWSAPIResponse) { /** * Responses to "subscribe" are quite basic, with no indication of errors (e.g. bad topic): * * { * result: null, * id: 1, * }; * * Currently there's no simple way to tell this apart from an actual WS API response with * error. So subscribe/unsubscribe requests will simply look like a WS API response internally, * but that will not affect usage. * * Unrelated, example wsapi error for reference: * { id: 1, status: 400, error: { code: -1021, msg: "Timestamp for this request was 1000ms ahead of the server's time." }, rateLimits: [ { rateLimitType: 'REQUEST_WEIGHT', interval: 'MINUTE', intervalNum: 1, limit: 6000, count: 4 } ], wsKey: 'mainWSAPI', isWSAPIResponse: true } */ const isError = typeof parsedEventErrorCode === 'number' && parsedEventErrorCode !== 0; // This is the counterpart to getPromiseRefForWSAPIRequest const promiseRef = [wsKey, parsedEventId].join('_'); if (!parsedEventId) { 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:', { wsKey, promiseRef, parsedEvent: eventData, }); } // WS API Exception if (isError) { try { this.getWsStore().rejectDeferredPromise(wsKey, promiseRef, Object.assign({ wsKey }, eventData), true); } catch (e) { this.logger.error('Exception trying to reject WSAPI promise', { wsKey, promiseRef, parsedEvent: eventData, e, }); } results.push({ eventType: 'exception', event: eventData, isWSAPIResponse: isWSAPIResponse, }); return results; } // authenticated if ((_b = eventData.result) === null || _b === void 0 ? void 0 : _b.apiKey) { // Note: Without this check, this will also trigger "onWsAuthenticated()" for session.status requests if (this.getWsStore().getAuthenticationInProgressPromise(wsKey)) { results.push({ eventType: 'authenticated', event: eventData, isWSAPIResponse: isWSAPIResponse, }); } } // WS API Success try { this.getWsStore().resolveDeferredPromise(wsKey, promiseRef, Object.assign({ wsKey }, eventData), true); } catch (e) { this.logger.error('Exception trying to resolve WSAPI promise', { wsKey, promiseRef, parsedEvent: eventData, e, }); } results.push({ eventType: 'response', event: Object.assign(Object.assign({}, eventData), { request: this.getCachedMidFlightRequest(wsKey, `${parsedEventId}`) }), isWSAPIResponse: isWSAPIResponse, }); this.removeCachedMidFlightRequest(wsKey, `${parsedEventId}`); return results; } // Handle incoming event that listen key expired if (eventType === 'listenKeyExpired') { const legacyContext = (0, websocket_util_1.getLegacyWsKeyContext)(wsKey); if (!legacyContext) { // handle this how? throw new Error('No context found within wsKey - fatal error with expired listen key'); } this.logger.info(`${legacyContext.market} listenKey EXPIRED - attempting to respawn user data stream: ${wsKey}`, eventData); // Just closing the connection (with the last parameter as true) will handle cleanup and respawn // Automatically leads to triggerCustomReconnectionWorkflow() to handle fresh user data respawn this.getUserDataStreamManager().teardownUserDataListenKey(legacyContext.listenKey, this.getWsStore().getWs(wsKey)); this.executeReconnectableClose(wsKey, 'listenKeyExpired'); } if (this.options.beautify) { const beautifiedMessage = this.beautifier.beautifyWsMessage(eventData, eventType, false, // Suffix all events for the beautifier, if market is options // Options has some conflicting keys with different intentions, so will be suffixed wsKey === 'eoptions' ? 'Options' : ''); results.push({ eventType: 'formattedMessage', event: beautifiedMessage, isWSAPIResponse, }); // emit an additional event for user data messages if (!Array.isArray(beautifiedMessage) && eventType) { if (websocket_util_1.EVENT_TYPES_USER_DATA.includes(eventType)) { results.push({ eventType: 'formattedUserDataMessage', event: beautifiedMessage, isWSAPIResponse, }); } } } // Messages for a subscribed topic all include the "topic" property if (typeof eventType === 'string') { results.push({ eventType: 'message', event: (eventData === null || eventData === void 0 ? void 0 : eventData.data) ? eventData.data : eventData, }); return results; } // Messages that are a "reply" to a request/command (e.g. subscribe to these topics) typically include the "op" property if (typeof eventType === 'string') { // Failed request if (eventData.success === false) { results.push({ eventType: 'exception', event: eventData, }); return results; } // These are request/reply pattern events (e.g. after subscribing to topics or authenticating) if (EVENTS_RESPONSES.includes(eventType)) { results.push({ eventType: 'response', event: eventData, }); return results; } // Request/reply pattern for authentication success if (EVENTS_AUTHENTICATED.includes(eventType)) { results.push({ eventType: 'authenticated', event: eventData, }); return results; } this.logger.error(`!! Unhandled string operation type "${eventType}". Defaulting to "update" channel... raw event:`, JSON.stringify(parsedEvent)); } else { this.logger.error(`!!!! Unhandled non-string event type "${eventType}". Defaulting to "update" channel... raw event:`, JSON.stringify(parsedEvent)); } // In case of catastrophic failure, fallback to noisy emit update results.push({ eventType: 'message', event: eventData, }); } catch (e) { results.push({ event: { message: 'Failed to parse event data due to exception', exception: e, eventData: event.data, }, eventType: 'exception', }); this.logger.error('Error caught within resolveEmittableEvents - failed to parse event data? Caught exception: ', { exception: e, eventData: event.data, }); } return results; } /** * * * * * * User Data Streams * * * * * */ /** * -------------------------- * User data listen key tracking & persistence * -------------------------- **/ /** * Subscribe to a spot user data stream. Use REST client to generate and persist listen key. * Supports spot, margin & isolated margin listen keys. */ subscribeSpotUserDataStreamWithListenKey(wsKey, listenKey, forceNewConnection, miscState) { return __awaiter(this, void 0, void 0, function* () { return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, 'spot', listenKey, forceNewConnection, miscState); }); } /** * Subscribe to spot user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group (e.g. port 9443 (main) vs 443 (main2)) */ subscribeSpotUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'main', forceNewConnection, miscState) { this.logger.trace('subscribeSpotUserDataStream()', { wsKey, forceNewConnection, miscState, }); try { const { listenKey } = yield this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .getSpotUserDataListenKey(); return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, 'spot', listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to spot user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribeSpotUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribeSpotUserDataStream(wsKey = 'main') { return this.closeUserDataStream(wsKey, 'spot'); } /** * Subscribe to margin user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group (e.g. port 9443 (main) vs 443 (main2)) */ subscribeCrossMarginUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'main', forceNewConnection, miscState) { try { const { listenKey } = yield this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .getMarginUserDataListenKey(); const market = 'crossMargin'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to margin user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribeMarginUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribeCrossMarginUserDataStream(wsKey = 'main') { return this.closeUserDataStream(wsKey, 'crossMargin'); } /** * Subscribe to isolated margin user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group (e.g. port 9443 (main) vs 443 (main2)) */ subscribeIsolatedMarginUserDataStream(symbol_1) { return __awaiter(this, arguments, void 0, function* (symbol, wsKey = 'main', forceNewConnection, miscState) { try { const lowerCaseSymbol = symbol.toLowerCase(); const { listenKey } = yield this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .getIsolatedMarginUserDataListenKey({ symbol: lowerCaseSymbol, }); const market = 'isolatedMargin'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, Object.assign(Object.assign({}, miscState), { symbol })); } catch (e) { this.logger.error('Failed to connect to isolated margin user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e, symbol })); this.emit('exception', { functionRef: 'subscribeIsolatedMarginUserDataStream()', wsKey, forceNewConnection, miscState: Object.assign(Object.assign({}, miscState), { symbol }), error: (e === null || e === void 0 ? void 0 : e.stack) || e, }); } }); } unsubscribeIsolatedMarginUserDataStream(symbol, wsKey = 'main') { return this.closeUserDataStream(wsKey, 'isolatedMargin', symbol); } /** * Subscribe to margin risk user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group (e.g. port 9443 (main) vs 443 (main2)) */ subscribeMarginRiskUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'main', forceNewConnection, miscState) { try { const { listenKey } = yield this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .getMarginRiskUserDataListenKey(); const market = 'riskDataMargin'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to margin risk user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribeMarginRiskUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribeMarginRiskUserDataStream(wsKey = 'main') { return this.closeUserDataStream(wsKey, 'riskDataMargin'); } /** * -------------------------- * End of SPOT market websocket streams * -------------------------- **/ /** * Subscribe to USD-M Futures user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group. */ subscribeUsdFuturesUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'usdm', // usdm | usdmTestnet forceNewConnection, miscState) { try { const isTestnet = wsKey === websocket_util_1.WS_KEY_MAP.usdmTestnet; const restClient = this.restClientCache.getUSDMRestClient(this.getRestClientOptions(), this.options.requestOptions); const { listenKey } = yield restClient.getFuturesUserDataListenKey(); const market = isTestnet ? 'usdmTestnet' : 'usdm'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to USD Futures user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribeUsdFuturesUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribeUsdFuturesUserDataStream(wsKey = 'usdm') { return this.closeUserDataStream(wsKey, 'usdm'); } /** * Subscribe to COIN-M Futures user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group. */ subscribeCoinFuturesUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'coinm', // coinm | coinmTestnet forceNewConnection, miscState) { try { const isTestnet = wsKey === websocket_util_1.WS_KEY_MAP.coinmTestnet; const { listenKey } = yield this.restClientCache .getCOINMRestClient(this.getRestClientOptions(), this.options.requestOptions) .getFuturesUserDataListenKey(); const market = isTestnet ? 'coinmTestnet' : 'coinm'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to COIN Futures user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribeCoinFuturesUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribeCoinFuturesUserDataStream(wsKey = 'coinm') { return this.closeUserDataStream(wsKey, 'coinm'); } /** * Subscribe to Portfolio Margin user data stream - listen key is automatically generated. Calling multiple times only opens one connection. * * Note: the wsKey parameter is optional, but can be used to connect to other environments for this product group. */ subscribePortfolioMarginUserDataStream() { return __awaiter(this, arguments, void 0, function* (wsKey = 'portfolioMarginUserData', forceNewConnection, miscState) { try { const { listenKey } = yield this.restClientCache .getPortfolioClient(this.getRestClientOptions(), this.options.requestOptions) .getPMUserDataListenKey(); const market = 'portfoliom'; return this.getUserDataStreamManager().subscribeGeneralUserDataStreamWithListenKey(wsKey, market, listenKey, forceNewConnection, miscState); } catch (e) { this.logger.error('Failed to connect to Portfolio Margin user data', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { error: e })); this.emit('exception', Object.assign(Object.assign({ functionRef: 'subscribePortfolioMarginUserDataStream()', wsKey, forceNewConnection }, miscState), { error: (e === null || e === void 0 ? void 0 : e.stack) || e })); } }); } unsubscribePortfolioMarginUserDataStream(wsKey = 'portfolioMarginUserData') { return this.closeUserDataStream(wsKey, 'portfoliom'); } /** * Close an active, dedicated, user data stream connection. * * @param wsKey - the connection key used to open the connection (excluding any automatic parameters such as the listen key). E.g. 'main' for spot/margin, 'usdm' for futures. * @param wsMarket - the product group, recommended if you're subscribed to both spot and margin (since they're on the same wsKey (main)). */ closeUserDataStream(wsKey, wsMarket, symbol) { return __awaiter(this, void 0, void 0, function* () { const wsKeys = this.getWsStore().getKeys(); const userDataWsKey = wsKeys.find((key) => { if (key === wsKey) { return true; } // built around the assumption in how per-connection listen key wskeys are created // isolatedMargin_userData_BTCUSDC_6RszN123x213x1233x213x1233x213xx123x1uzkTV_main // coinm_userData__WRAVTxGaQa1Nhd1243312kjn13kj12n3m5wRFv6JoFQgwUR5AEFofZtlk_coinm const symbolSuffix = symbol ? '_' + symbol : ''; const prefixMatch = wsMarket + '_userData' + symbolSuffix; return key.startsWith(prefixMatch) && key.endsWith(wsKey); }); if (!userDataWsKey) {