binance
Version:
Professional Node.js & JavaScript SDK for Binance REST APIs & WebSockets, with TypeScript & end-to-end tests.
969 lines • 58.5 kB
JavaScript
"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