UNPKG

binance

Version:

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

969 lines 58.5 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.WebsocketClientV1 = void 0; /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ const events_1 = require("events"); const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const beautifier_1 = __importDefault(require("./util/beautifier")); const logger_1 = require("./util/logger"); const requestUtils_1 = require("./util/requestUtils"); const typeGuards_1 = require("./util/typeGuards"); const listen_key_state_cache_1 = require("./util/websockets/listen-key-state-cache"); const rest_client_cache_1 = require("./util/websockets/rest-client-cache"); const websocket_util_1 = require("./util/websockets/websocket-util"); const WsStore_1 = require("./util/websockets/WsStore"); const WsStore_types_1 = require("./util/websockets/WsStore.types"); const wsBaseEndpoints = { spot: 'wss://stream.binance.com:9443', crossMargin: 'wss://stream.binance.com:9443', isolatedMargin: 'wss://stream.binance.com:9443', usdm: 'wss://fstream.binance.com', usdmTestnet: 'wss://stream.binancefuture.com', coinm: 'wss://dstream.binance.com', coinmTestnet: 'wss://dstream.binancefuture.com', options: 'wss://vstream.binance.com', optionsTestnet: 'wss://testnetws.binanceops.com', riskDataMargin: '', spotTestnet: '', portfoliom: '', }; /** * @deprecated This legacy websocket client creates one websocket connection per topic. * * If subscribing to a lot of topics, consider using the new multiplex `WebsocketClient`. * * To split your topics into smaller groups (one connection per group), simply make multiple multiplex WebsocketClient instances. */ class WebsocketClientV1 extends events_1.EventEmitter { constructor(options, logger) { super(); this.beautifier = new beautifier_1.default({ warnKeyMissingInMap: false, }); this.restClientCache = new rest_client_cache_1.RestClientCache(); this.logger = logger || logger_1.DefaultLogger; this.wsStore = new WsStore_1.WsStore(this.logger); this.listenKeyStateCache = new listen_key_state_cache_1.ListenKeyStateCache(this.logger); this.options = Object.assign({ // Some defaults: pongTimeout: 7500, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 5000, // Automatically send an authentication op/request after a connection opens, for private connections. authPrivateConnectionsOnConnect: false, // Individual requests require a signature authPrivateRequests: true }, options); this.wsUrlKeyMap = {}; // add default error handling so this doesn't crash node (if the user didn't set a handler) this.on('error', () => { }); } 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 }); } connectToWsUrl(url, wsKey, forceNewConnection) { const wsRefKey = wsKey || url; const oldWs = this.wsStore.getWs(wsRefKey); if (oldWs && this.wsStore.isWsOpen(wsRefKey) && !forceNewConnection) { this.logger.trace('connectToWsUrl(): Returning existing open WS connection', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsRefKey })); return oldWs; } this.logger.trace(`connectToWsUrl(): Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsRefKey })); const _a = this.options.wsOptions || {}, { protocols = [] } = _a, wsOptions = __rest(_a, ["protocols"]); const ws = new isomorphic_ws_1.default(url, protocols, wsOptions); this.wsUrlKeyMap[url] = wsRefKey; if (typeof ws.on === 'function') { ws.on('ping', (event) => this.onWsPing(event, wsRefKey, ws, 'event')); ws.on('pong', (event) => this.onWsPong(event, wsRefKey, 'event')); } ws.onopen = (event) => this.onWsOpen(event, wsRefKey, url); ws.onerror = (event) => this.parseWsError('WS Error Event', event, wsRefKey, url); ws.onclose = (event) => this.onWsClose(event, wsRefKey, ws, url); ws.onmessage = (event) => this.onWsMessage(event, wsRefKey, 'function'); // Not sure these work in the browser, the traditional event listeners are required for ping/pong frames in node ws.onping = (event) => this.onWsPing(event, wsRefKey, ws, 'function'); ws.onpong = (event) => this.onWsPong(event, wsRefKey, 'function'); // Add ws connection with key to store this.wsStore.setWs(wsRefKey, ws); ws.wsKey = wsRefKey; return ws; } tryWsSend(wsKey, wsMessage) { try { this.logger.trace('Sending upstream ws message: ', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsMessage, wsKey })); if (!wsKey) { throw new Error('No wsKey provided'); } const ws = this.getWs(wsKey); if (!ws) { throw new Error(`No active websocket connection exists for wsKey: ${wsKey}`); } ws.send(wsMessage); } catch (e) { this.logger.error('Failed to send WS message', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsMessage, wsKey, exception: e })); } } tryWsPing(wsKey) { try { // this.logger.trace(`Sending upstream ping: `, { ...loggerCategory, wsKey }); if (!wsKey) { throw new Error('No wsKey provided'); } const ws = this.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({}, websocket_util_1.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({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, exception: e })); } } onWsOpen(ws, wsKey, wsUrl) { this.logger.trace(`onWsOpen(): ${wsUrl} : ${wsKey}`); if (this.wsStore.isConnectionState(wsKey, WsStore_types_1.WsConnectionStateEnum.RECONNECTING)) { this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey })); this.emit('reconnected', { wsKey, ws }); } else { this.logger.info('Websocket connected', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey })); this.emit('open', { wsKey, ws }); } this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.CONNECTED); const topics = [...this.wsStore.getTopics(wsKey)]; if (topics.length) { this.requestSubscribeTopics(wsKey, topics); } if (!this.options.disableHeartbeat) { const wsState = this.wsStore.get(wsKey, true); if (wsState.activePingTimer) { clearInterval(wsState.activePingTimer); } wsState.activePingTimer = setInterval(() => this.sendPing(wsKey, wsUrl), this.options.pingInterval); } } onWsClose(event, wsKey, ws, wsUrl) { var _a; const wsConnectionState = this.wsStore.getConnectionState(wsKey); const { market, listenKey, isUserData } = (0, websocket_util_1.getContextFromWsKey)(wsKey); this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, eventCloseCode: (_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a._closeCode, wsConnectionState, isUserData, listenKey, market })); // Clear any timers before we initiate revival this.clearTimers(wsKey); // User data sockets include the listen key. To prevent accummulation in memory we should clean up old disconnected states if (isUserData) { this.wsStore.delete(wsKey); if (listenKey) { this.listenKeyStateCache.clearAllListenKeyState(listenKey); } } if (wsConnectionState !== WsStore_types_1.WsConnectionStateEnum.CLOSING) { this.reconnectWithDelay(wsKey, this.options.reconnectTimeout, wsUrl); this.emit('reconnecting', { wsKey, event, ws }); } else { this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.INITIAL); this.emit('close', { wsKey, event, ws }); } } onWsMessage(event, wsKey, source) { try { this.clearPongTimer(wsKey); const msg = (0, websocket_util_1.parseRawWsMessageLegacy)(event, this.options); // Edge case where raw event does not include event type, detect using wsKey and mutate msg.e const eventType = (0, websocket_util_1.parseEventTypeFromMessage)(wsKey, msg); (0, requestUtils_1.appendEventIfMissing)(msg, wsKey, eventType); (0, websocket_util_1.appendEventMarket)(msg, wsKey); if (eventType) { this.emit('message', msg); if (eventType === 'listenKeyExpired') { const { market } = (0, websocket_util_1.getContextFromWsKey)(wsKey); this.logger.info(`${market} listenKey expired - attempting to respawn user data stream: ${wsKey}`); // Just closing the connection (with the last parameter as true) will handle cleanup and respawn const shouldTriggerReconnect = true; this.close(wsKey, shouldTriggerReconnect); } if (this.options.beautify) { const beautifiedMessage = this.beautifier.beautifyWsMessage(msg, eventType, false); this.emit('formattedMessage', beautifiedMessage); // emit a separate event for user data messages if (!Array.isArray(beautifiedMessage)) { if ([ 'balanceUpdate', 'executionReport', 'listStatus', 'listenKeyExpired', 'outboundAccountPosition', 'ACCOUNT_CONFIG_UPDATE', 'ACCOUNT_UPDATE', 'MARGIN_CALL', 'ORDER_TRADE_UPDATE', 'TRADE_LITE', 'CONDITIONAL_ORDER_TRIGGER_REJECT', ].includes(eventType)) { this.emit('formattedUserDataMessage', beautifiedMessage); } } } return; } if (msg.result !== undefined) { this.emit('reply', { type: event.type, data: msg, wsKey, }); return; } this.logger.error('Bug? Unhandled ws message event type. Check if appendEventIfMissing needs to parse wsKey.', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { parsedMessage: JSON.stringify(msg), rawEvent: event, wsKey, source })); } catch (e) { this.logger.error('Exception parsing ws message: ', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { rawEvent: event, wsKey, error: e, source })); this.emit('error', { wsKey, error: e, rawEvent: event, source }); } } sendPing(wsKey, wsUrl) { this.clearPongTimer(wsKey); this.logger.trace('Sending ping', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey })); this.tryWsPing(wsKey); this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => this.executeReconnectableClose(wsKey, 'Pong timeout', wsUrl), this.options.pongTimeout); } onWsPing(event, wsKey, ws, source) { this.logger.trace('Received ping, sending pong frame', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, source })); ws.pong(); } onWsPong(event, wsKey, source) { this.logger.trace('Received pong, clearing pong timer', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, source })); this.clearPongTimer(wsKey); } /** * Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously. * If closed, trigger a reconnect immediately */ executeReconnectableClose(wsKey, reason, wsUrl) { this.logger.info(new Date(), `${reason} - closing socket to reconnect`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, reason })); const wasOpen = this.wsStore.isWsOpen(wsKey); (0, websocket_util_1.safeTerminateWs)(this.getWs(wsKey), true); this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); if (!wasOpen) { this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, reason })); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout, wsUrl); } } close(wsKey, shouldReconnectAfterClose) { var _a; this.logger.info('Closing connection', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, willReconnect: shouldReconnectAfterClose })); this.setWsState(wsKey, shouldReconnectAfterClose ? WsStore_types_1.WsConnectionStateEnum.RECONNECTING : WsStore_types_1.WsConnectionStateEnum.CLOSING); this.clearTimers(wsKey); (_a = this.getWs(wsKey)) === null || _a === void 0 ? void 0 : _a.close(); const { listenKey } = (0, websocket_util_1.getContextFromWsKey)(wsKey); if (listenKey) { this.teardownUserDataListenKey(listenKey, this.getWs(wsKey)); } else { (0, websocket_util_1.safeTerminateWs)(this.getWs(wsKey), true); } } closeAll(shouldReconnectAfterClose) { const keys = this.wsStore.getKeys(); this.logger.info(`Closing all ws connections: ${keys}`); keys.forEach((key) => { this.close(key, shouldReconnectAfterClose); }); } closeWs(ws, shouldReconnectAfterClose) { const wsKey = this.wsUrlKeyMap[ws.url] || (ws === null || ws === void 0 ? void 0 : ws.wsKey); if (!wsKey) { throw new Error('Cannot close websocket as it has no known wsKey attached.'); } return this.close(wsKey, shouldReconnectAfterClose); } parseWsError(context, error, wsKey, wsUrl) { this.logger.error(context, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, error })); if (!error.message) { this.logger.error(`${context} due to unexpected error: `, error); this.emit('error', { error, wsKey, wsUrl }); return; } switch (error.message) { case 'Unexpected server response: 401': this.logger.error(`${context} due to 401 authorization failure.`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey })); break; default: if (this.wsStore.getConnectionState(wsKey) !== WsStore_types_1.WsConnectionStateEnum.CLOSING) { this.logger.error(`${context} due to unexpected response error: "${(error === null || error === void 0 ? void 0 : error.msg) || (error === null || error === void 0 ? void 0 : error.message) || error}"`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, error })); this.executeReconnectableClose(wsKey, 'unhandled onWsError', wsUrl); } else { this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`); } break; } this.emit('error', { error, wsKey, wsUrl }); } reconnectWithDelay(wsKey, connectionDelayMs, wsUrl) { var _a; this.clearTimers(wsKey); if (this.wsStore.getConnectionState(wsKey) !== WsStore_types_1.WsConnectionStateEnum.CONNECTING) { this.setWsState(wsKey, WsStore_types_1.WsConnectionStateEnum.RECONNECTING); } this.logger.info('Reconnecting to websocket with delay...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, connectionDelayMs })); if ((_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.activeReconnectTimer) { this.clearReconnectTimer(wsKey); } this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.clearReconnectTimer(wsKey); if (wsKey.includes('userData')) { const { market, symbol, isTestnet } = (0, websocket_util_1.getContextFromWsKey)(wsKey); this.logger.info('Reconnecting to user data stream', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, market, symbol })); // We'll set a new one once the new stream respawns, with a diff listenKey in the key this.wsStore.delete(wsKey); this.respawnUserDataStream(market, symbol, isTestnet); return; } this.logger.info('Reconnecting to public websocket', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { wsKey, wsUrl })); this.connectToWsUrl(wsUrl, wsKey); }, connectionDelayMs); } clearTimers(wsKey) { this.clearPingTimer(wsKey); this.clearPongTimer(wsKey); this.clearReconnectTimer(wsKey); } // Send a ping at intervals clearPingTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState === null || wsState === void 0 ? void 0 : 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 === null || wsState === void 0 ? void 0 : wsState.activePongTimer) { clearTimeout(wsState.activePongTimer); wsState.activePongTimer = undefined; } } // Timer tracking that a reconnect is about to happen / in progress clearReconnectTimer(wsKey) { const wsState = this.wsStore.get(wsKey); if (wsState === null || wsState === void 0 ? void 0 : wsState.activeReconnectTimer) { clearTimeout(wsState.activeReconnectTimer); wsState.activeReconnectTimer = undefined; } } // eslint-disable-next-line @typescript-eslint/no-unused-vars getWsBaseUrl(market, wsKey) { if (this.options.wsUrl) { return this.options.wsUrl; } return wsBaseEndpoints[market]; } getWs(wsKey) { return this.wsStore.getWs(wsKey); } setWsState(wsKey, state) { this.wsStore.setConnectionState(wsKey, state); } /** * Send WS message to subscribe to topics. Use subscribe() to call this. */ requestSubscribeTopics(wsKey, topics) { const wsMessage = JSON.stringify({ method: 'SUBSCRIBE', params: topics, id: new Date().getTime(), }); this.tryWsSend(wsKey, wsMessage); } /** * Send WS message to unsubscribe from topics. Use unsubscribe() to call this. */ requestUnsubscribeTopics(wsKey, topics) { const wsMessage = JSON.stringify({ op: 'UNSUBSCRIBE', params: topics, id: new Date().getTime(), }); this.tryWsSend(wsKey, wsMessage); } /** * Send WS message to unsubscribe from topics. */ requestListSubscriptions(wsKey, requestId) { const wsMessage = JSON.stringify({ method: 'LIST_SUBSCRIPTIONS', id: requestId, }); this.tryWsSend(wsKey, wsMessage); } /** * Send WS message to set property state */ requestSetProperty(wsKey, property, value, requestId) { const wsMessage = JSON.stringify({ method: 'SET_PROPERTY', params: [property, value], id: requestId, }); this.tryWsSend(wsKey, wsMessage); } /** * Send WS message to get property state */ requestGetProperty(wsKey, property, requestId) { const wsMessage = JSON.stringify({ method: 'GET_PROPERTY', params: [property], id: requestId, }); this.tryWsSend(wsKey, wsMessage); } /** * -------------------------- * User data listen key tracking & persistence * -------------------------- **/ setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey, symbol, isTestnet) { this.listenKeyStateCache.clearAllListenKeyState(listenKey); const listenKeyState = this.listenKeyStateCache.getListenKeyState(listenKey, market); this.logger.trace(`Created new listen key interval timer for ${listenKey}`); // Set timer to keep WS alive every 50 minutes const minutes50 = 1000 * 60 * 50; listenKeyState.keepAliveTimer = setInterval(() => this.checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol, isTestnet), minutes50); } sendKeepAliveForMarket(listenKey, market, ws, wsKey, symbol, isTestnet) { switch (market) { case 'spot': return this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .keepAliveSpotUserDataListenKey(listenKey); case 'spotTestnet': return this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .keepAliveSpotUserDataListenKey(listenKey); case 'crossMargin': return this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .keepAliveMarginUserDataListenKey(listenKey); case 'isolatedMargin': return this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .keepAliveIsolatedMarginUserDataListenKey({ listenKey, symbol: symbol, }); case 'coinm': case 'options': case 'optionsTestnet': case 'usdm': return this.restClientCache .getUSDMRestClient(this.getRestClientOptions(), this.options.requestOptions) .keepAliveFuturesUserDataListenKey(); case 'usdmTestnet': return this.restClientCache .getUSDMRestClient(Object.assign(Object.assign({}, this.getRestClientOptions()), { testnet: isTestnet }), this.options.requestOptions) .keepAliveFuturesUserDataListenKey(); case 'coinmTestnet': return this.restClientCache .getCOINMRestClient(Object.assign(Object.assign({}, this.getRestClientOptions()), { testnet: isTestnet }), this.options.requestOptions) .keepAliveFuturesUserDataListenKey(); case 'portfoliom': return this.restClientCache .getPortfolioClient(this.getRestClientOptions(), this.options.requestOptions) .keepAlivePMUserDataListenKey(); case 'riskDataMargin': { throw new Error('Unsupported user data stream. Use the new "WebsocketClient" to use this stream.'); } default: throw (0, typeGuards_1.neverGuard)(market, `Failed to send keep alive for user data stream in unhandled market ${market}`); } } checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol, isTestnet) { return __awaiter(this, void 0, void 0, function* () { const listenKeyState = this.listenKeyStateCache.getListenKeyState(listenKey, market); try { if (listenKeyState.keepAliveRetryTimer) { clearTimeout(listenKeyState.keepAliveRetryTimer); listenKeyState.keepAliveRetryTimer = undefined; } // Simple way to test keep alive failure handling: // throw new Error(`Fake keep alive failure`); yield this.sendKeepAliveForMarket(listenKey, market, ws, wsKey, symbol, isTestnet); listenKeyState.lastKeepAlive = Date.now(); listenKeyState.keepAliveFailures = 0; this.logger.info(`Completed keep alive cycle for listenKey(${listenKey}) in market(${market})`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey })); } catch (e) { listenKeyState.keepAliveFailures++; // code: -1125, // message: 'This listenKey does not exist.', const errorCode = e === null || e === void 0 ? void 0 : e.code; if (errorCode === -1125) { this.logger.error('FATAL: Failed to keep WS alive for listen key - listen key expired/invalid. Respawning with fresh listen key...', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e, errorCode, errorMsg: e === null || e === void 0 ? void 0 : e.message })); const shouldReconnectAfterClose = false; this.close(wsKey, shouldReconnectAfterClose); this.respawnUserDataStream(market, symbol); return; } // If max failurees reached, tear down and respawn if allowed if (listenKeyState.keepAliveFailures >= 3) { this.logger.error('FATAL: Failed to keep WS alive for listen key after 3 attempts', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e })); // reconnect follows a less automatic workflow since this is tied to a listen key (which may need a new one). // Kill connection first, with instruction NOT to reconnect automatically const shouldReconnectAfterClose = false; this.close(wsKey, shouldReconnectAfterClose); // Then respawn a connection with a potentially new listen key (since the old one may be invalid now) this.respawnUserDataStream(market, symbol); return; } const reconnectDelaySeconds = 1000 * 15; this.logger.info(`Userdata keep alive request failed due to error, trying again with short delay (${reconnectDelaySeconds} seconds)`, Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { listenKey, error: e, keepAliveAttempts: listenKeyState.keepAliveFailures })); listenKeyState.keepAliveRetryTimer = setTimeout(() => this.checkKeepAliveListenKey(listenKey, market, ws, wsKey, symbol), reconnectDelaySeconds); } }); } teardownUserDataListenKey(listenKey, ws) { if (listenKey) { this.listenKeyStateCache.clearAllListenKeyState(listenKey); (0, websocket_util_1.safeTerminateWs)(ws, true); } } respawnUserDataStream(market, symbol, isTestnet, respawnAttempt) { return __awaiter(this, void 0, void 0, function* () { // If another connection attempt is in progress for this listen key, don't initiate a retry or the risk is multiple connections on the same listen key const forceNewConnection = false; const isReconnecting = true; let ws; try { switch (market) { case 'spot': ws = yield this.subscribeSpotUserDataStream(forceNewConnection, isReconnecting); break; case 'crossMargin': ws = yield this.subscribeMarginUserDataStream(forceNewConnection, isReconnecting); break; case 'isolatedMargin': ws = yield this.subscribeIsolatedMarginUserDataStream(symbol, forceNewConnection, isReconnecting); break; case 'usdm': ws = yield this.subscribeUsdFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting); break; case 'usdmTestnet': ws = yield this.subscribeUsdFuturesUserDataStream(true, forceNewConnection, isReconnecting); break; case 'coinm': ws = yield this.subscribeCoinFuturesUserDataStream(isTestnet, forceNewConnection, isReconnecting); break; case 'coinmTestnet': ws = yield this.subscribeCoinFuturesUserDataStream(true, forceNewConnection, isReconnecting); break; case 'portfoliom': case 'spotTestnet': case 'options': case 'optionsTestnet': case 'riskDataMargin': { throw new Error('Unsupported user data stream. Use the new "WebsocketClient" to use this stream.'); } default: throw (0, typeGuards_1.neverGuard)(market, `Failed to respawn user data stream - unhandled market: ${market}`); } } catch (e) { this.logger.error('Exception trying to spawn user data stream', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { market, symbol, isTestnet, error: e })); this.emit('error', { wsKey: market + '_' + 'userData', error: e }); } if (!ws) { const delayInSeconds = 2; this.logger.error('User key respawn failed, trying again with short delay', Object.assign(Object.assign({}, websocket_util_1.WS_LOGGER_CATEGORY), { market, symbol, isTestnet, respawnAttempt, delayInSeconds })); // This timer should probably be tracked/singleton setTimeout(() => this.respawnUserDataStream(market, symbol, isTestnet, respawnAttempt ? respawnAttempt + 1 : 1), 1000 * delayInSeconds); } }); } /** * -------------------------- * Universal market websocket streams (may apply to one or more API markets) * -------------------------- **/ /** * Subscribe to a universal market websocket stream */ subscribeEndpoint(endpoint, market, forceNewConnection) { const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, endpoint); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${endpoint}`, wsKey, forceNewConnection); } /** * Subscribe to aggregate trades for a symbol in a market category */ subscribeAggregateTrades(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'aggTrade'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to trades for a symbol in a market category * IMPORTANT: This topic for usdm and coinm is not listed in the api docs and might stop working without warning */ subscribeTrades(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'trade'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to coin index for a symbol in COINM Futures markets */ subscribeCoinIndexPrice(symbol, updateSpeedMs = 3000, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'indexPrice'; const speedSuffix = updateSpeedMs === 1000 ? '@1s' : ''; const market = 'coinm'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}${speedSuffix}`, wsKey, forceNewConnection); } /** * Subscribe to mark price for a symbol in a market category */ subscribeMarkPrice(symbol, market, updateSpeedMs = 3000, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'markPrice'; const speedSuffix = updateSpeedMs === 1000 ? '@1s' : ''; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}${speedSuffix}`, wsKey, forceNewConnection); } /** * Subscribe to mark price for all symbols in a market category */ subscribeAllMarketMarkPrice(market, updateSpeedMs = 3000, forceNewConnection) { const streamName = '!markPrice@arr'; const speedSuffix = updateSpeedMs === 1000 ? '@1s' : ''; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${streamName}${speedSuffix}`, wsKey, forceNewConnection); } /** * Subscribe to klines(candles) for a symbol in a market category */ subscribeKlines(symbol, interval, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'kline'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol, interval); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection); } /** * Subscribe to continuous contract klines(candles) for a symbol futures */ subscribeContinuousContractKlines(symbol, contractType, interval, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'continuousKline'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol, interval); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}_${contractType}@${streamName}_${interval}`, wsKey, forceNewConnection); } /** * Subscribe to index klines(candles) for a symbol in a coinm futures */ subscribeIndexKlines(symbol, interval, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'indexPriceKline'; const market = 'coinm'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol, interval); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection); } /** * Subscribe to index klines(candles) for a symbol in a coinm futures */ subscribeMarkPriceKlines(symbol, interval, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'markPrice_kline'; const market = 'coinm'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol, interval); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}_${interval}`, wsKey, forceNewConnection); } /** * Subscribe to mini 24hr ticker for a symbol in market category. */ subscribeSymbolMini24hrTicker(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'miniTicker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to mini 24hr mini ticker in market category. */ subscribeAllMini24hrTickers(market, forceNewConnection) { const streamName = 'miniTicker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}@arr`, wsKey, forceNewConnection); } /** * Subscribe to 24hr ticker for a symbol in any market. */ subscribeSymbol24hrTicker(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'ticker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to 24hr ticker in any market. */ subscribeAll24hrTickers(market, forceNewConnection) { const streamName = 'ticker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}@arr`, wsKey, forceNewConnection); } /** * Subscribe to rolling window ticker statistics for all market symbols, * computed over multiple windows. Note that only tickers that have * changed will be present in the array. * * Notes: * - Supported window sizes: 1h,4h,1d. * - Supported markets: spot */ subscribeAllRollingWindowTickers(market, windowSize, forceNewConnection) { const streamName = 'ticker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, windowSize); const wsUrl = this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}_${windowSize}@arr`; return this.connectToWsUrl(wsUrl, wsKey, forceNewConnection); } /** * Subscribe to best bid/ask for symbol in spot markets. */ subscribeSymbolBookTicker(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'bookTicker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to best bid/ask for all symbols in spot markets. */ subscribeAllBookTickers(market, forceNewConnection) { const streamName = 'bookTicker'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to best bid/ask for symbol in spot markets. */ subscribeSymbolLiquidationOrders(symbol, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'forceOrder'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to best bid/ask for all symbols in spot markets. */ subscribeAllLiquidationOrders(market, forceNewConnection) { const streamName = 'forceOrder@arr'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/!${streamName}`, wsKey, forceNewConnection); } /** * Subscribe to partial book depths (snapshots). * * Note: * - spot only supports 1000ms or 100ms for updateMs * - futures only support 100, 250 or 500ms for updateMs * * Use getContextFromWsKey(data.wsKey) to extract symbol from events */ subscribePartialBookDepths(symbol, levels, updateMs, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'depth'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName, lowerCaseSymbol); const updateMsSuffx = typeof updateMs === 'number' ? `@${updateMs}ms` : ''; return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}${levels}${updateMsSuffx}`, wsKey, forceNewConnection); } /** * Subscribe to orderbook depth updates to locally manage an order book. * * Note that the updatems parameter depends on which market you're trading * * - Spot: https://binance-docs.github.io/apidocs/spot/en/#diff-depth-stream * - USDM Futures: https://binance-docs.github.io/apidocs/futures/en/#diff-book-depth-streams * * Use getContextFromWsKey(data.wsKey) to extract symbol from events */ subscribeDiffBookDepth(symbol, updateMs = 100, market, forceNewConnection) { const lowerCaseSymbol = symbol.toLowerCase(); const streamName = 'diffBookDepth'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, 'diffBookDepth', lowerCaseSymbol, String(updateMs)); const updateMsSuffx = typeof updateMs === 'number' ? `@${updateMs}ms` : ''; return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${lowerCaseSymbol}@${streamName}${updateMsSuffx}`, wsKey, forceNewConnection); } /** * Subscribe to best bid/ask for all symbols in spot markets. */ subscribeContractInfoStream(market, forceNewConnection) { const streamName = '!contractInfo'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, streamName); return this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${streamName}`, wsKey, forceNewConnection); } /** * -------------------------- * SPOT market websocket streams * -------------------------- **/ /** * Subscribe to aggregate trades for a symbol in spot markets. */ subscribeSpotAggregateTrades(symbol, forceNewConnection) { return this.subscribeAggregateTrades(symbol, 'spot', forceNewConnection); } /** * Subscribe to trades for a symbol in spot markets. */ subscribeSpotTrades(symbol, forceNewConnection) { return this.subscribeTrades(symbol, 'spot', forceNewConnection); } /** * Subscribe to candles for a symbol in spot markets. */ subscribeSpotKline(symbol, interval, forceNewConnection) { return this.subscribeKlines(symbol, interval, 'spot', forceNewConnection); } /** * Subscribe to mini 24hr ticker for a symbol in spot markets. */ subscribeSpotSymbolMini24hrTicker(symbol, forceNewConnection) { return this.subscribeSymbolMini24hrTicker(symbol, 'spot', forceNewConnection); } /** * Subscribe to mini 24hr mini ticker in spot markets. */ subscribeSpotAllMini24hrTickers(forceNewConnection) { return this.subscribeAllMini24hrTickers('spot', forceNewConnection); } /** * Subscribe to 24hr ticker for a symbol in spot markets. */ subscribeSpotSymbol24hrTicker(symbol, forceNewConnection) { return this.subscribeSymbol24hrTicker(symbol, 'spot', forceNewConnection); } /** * Subscribe to 24hr ticker in spot markets. */ subscribeSpotAll24hrTickers(forceNewConnection) { return this.subscribeAll24hrTickers('spot', forceNewConnection); } /** * Subscribe to best bid/ask for symbol in spot markets. */ subscribeSpotSymbolBookTicker(symbol, forceNewConnection) { return this.subscribeSymbolBookTicker(symbol, 'spot', forceNewConnection); } /** * Subscribe to best bid/ask for all symbols in spot markets. */ subscribeSpotAllBookTickers(forceNewConnection) { return this.subscribeAllBookTickers('spot', forceNewConnection); } /** * Subscribe to top bid/ask levels for symbol in spot markets. */ subscribeSpotPartialBookDepth(symbol, levels, updateMs = 1000, forceNewConnection) { return this.subscribePartialBookDepths(symbol, levels, updateMs, 'spot', forceNewConnection); } /** * Subscribe to spot orderbook depth updates to locally manage an order book. */ subscribeSpotDiffBookDepth(symbol, updateMs = 1000, forceNewConnection) { return this.subscribeDiffBookDepth(symbol, updateMs, 'spot', forceNewConnection); } /** * Subscribe to a spot user data stream. Use REST client to generate and persist listen key. * Supports spot, margin & isolated margin listen keys. */ subscribeSpotUserDataStreamWithListenKey(listenKey, forceNewConnection, isReconnecting) { const market = 'spot'; const wsKey = (0, websocket_util_1.getLegacyWsStoreKeyWithContext)(market, 'userData', undefined, listenKey); if (!forceNewConnection && this.wsStore.isConnectionAttemptInProgress(wsKey)) { this.logger.trace('Existing spot user data connection in progress for listen key. Avoiding duplicate'); return this.getWs(wsKey); } this.setWsState(wsKey, isReconnecting ? WsStore_types_1.WsConnectionStateEnum.RECONNECTING : WsStore_types_1.WsConnectionStateEnum.CONNECTING); const ws = this.connectToWsUrl(this.getWsBaseUrl(market, wsKey) + `/ws/${listenKey}`, wsKey, forceNewConnection); // Start & store timer to keep alive listen key (and handle expiration) this.setKeepAliveListenKeyTimer(listenKey, market, ws, wsKey); return ws; } /** * Subscribe to spot user data stream - listen key is automaticallyr generated. Calling multiple times only opens one connection. */ subscribeSpotUserDataStream(forceNewConnection, isReconnecting) { return __awaiter(this, void 0, void 0, function* () { try { const { listenKey } = yield this.restClientCache .getSpotRestClient(this.getRestClientOptions(), this.options.requestOptions) .getSpotUserDataListenKey(); return this.subscribeSpotUserDataStreamWithLi