UNPKG

binance

Version:

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

610 lines 24.8 kB
"use strict"; 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.WS_ERROR_ENUM = exports.WS_AUTH_ON_CONNECT_KEYS = exports.WS_KEY_MM_URL_MAP = exports.WS_KEY_URL_MAP = exports.WS_KEYS_FUTURES = exports.WS_KEYS_SPOT = exports.WS_KEY_MAP = exports.EVENT_TYPES_USER_DATA = exports.WS_LOGGER_CATEGORY = void 0; exports.getWsURLSuffix = getWsURLSuffix; exports.isPrivateWsTopic = isPrivateWsTopic; exports.getTestnetWsKey = getTestnetWsKey; exports.getWsUrl = getWsUrl; exports.getMaxTopicsPerSubscribeEvent = getMaxTopicsPerSubscribeEvent; exports.safeTerminateWs = safeTerminateWs; exports.getPromiseRefForWSAPIRequest = getPromiseRefForWSAPIRequest; exports.getNormalisedTopicRequests = getNormalisedTopicRequests; exports.getTopicsPerWSKey = getTopicsPerWSKey; exports.parseEventTypeFromMessage = parseEventTypeFromMessage; exports.resolveUserDataMarketForWsKey = resolveUserDataMarketForWsKey; exports.resolveWsKeyForLegacyMarket = resolveWsKeyForLegacyMarket; exports.parseRawWsMessageLegacy = parseRawWsMessageLegacy; exports.parseRawWsMessage = parseRawWsMessage; exports.getContextFromWsKey = getContextFromWsKey; exports.getLegacyWsStoreKeyWithContext = getLegacyWsStoreKeyWithContext; exports.getLegacyWsKeyContext = getLegacyWsKeyContext; exports.getRealWsKeyFromDerivedWsKey = getRealWsKeyFromDerivedWsKey; exports.appendEventMarket = appendEventMarket; exports.isWSPingFrameAvailable = isWSPingFrameAvailable; exports.isWSPongFrameAvailable = isWSPongFrameAvailable; /* eslint-disable @typescript-eslint/no-unused-vars */ const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); const typeGuards_1 = require("../typeGuards"); exports.WS_LOGGER_CATEGORY = { category: 'binance-ws' }; exports.EVENT_TYPES_USER_DATA = [ 'balanceUpdate', 'executionReport', 'listStatus', 'listenKeyExpired', 'outboundAccountPosition', 'ACCOUNT_CONFIG_UPDATE', 'ACCOUNT_UPDATE', 'MARGIN_CALL', 'ORDER_TRADE_UPDATE', 'TRADE_LITE', 'CONDITIONAL_ORDER_TRIGGER_REJECT', ]; exports.WS_KEY_MAP = { // https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams main: 'main', // spot, margin, isolated margin, user data main2: 'main2', // spot, margin, isolated margin, user data | alternative main3: 'main3', // spot, margin, isolated margin | alternative | MARKET DATA ONLY | NO USER DATA // https://developers.binance.com/docs/binance-spot-api-docs/testnet/web-socket-streams#general-wss-information mainTestnetPublic: 'mainTestnetPublic', mainTestnetUserData: 'mainTestnetUserData', // https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/general-api-information mainWSAPI: 'mainWSAPI', // trading over WS in spot, margin, isolated margin. User data supported too. mainWSAPI2: 'mainWSAPI2', // trading over WS in spot, margin, isolated margin. User data supported too. mainWSAPITestnet: 'mainWSAPITestnet', // trading over WS in spot, margin, isolated margin | TESTNET // https://developers.binance.com/docs/margin_trading/risk-data-stream marginRiskUserData: 'marginRiskUserData', // https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams // market data, user data usdm: 'usdm', // https://developers.binance.com/docs/derivatives/usds-margined-futures/general-info usdmTestnet: 'usdmTestnet', // https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-api-general-info // ONLY WS API | NO USER DATA usdmWSAPI: 'usdmWSAPI', usdmWSAPITestnet: 'usdmWSAPITestnet', // https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams // market data, user data coinm: 'coinm', coinm2: 'coinm2', // https://developers.binance.com/docs/derivatives/coin-margined-futures/general-info coinmTestnet: 'coinmTestnet', // https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-api-general-info // ONLY WS API | NO USER DATA coinmWSAPI: 'coinmWSAPI', coinmWSAPITestnet: 'coinmWSAPITestnet', eoptions: 'eoptions', // optionsTestnet: 'optionsTestnet', // https://developers.binance.com/docs/derivatives/portfolio-margin/user-data-streams portfolioMarginUserData: 'portfolioMarginUserData', // https://developers.binance.com/docs/derivatives/portfolio-margin-pro/portfolio-margin-pro-user-data-stream portfolioMarginProUserData: 'portfolioMarginProUserData', }; exports.WS_KEYS_SPOT = [ exports.WS_KEY_MAP.main, exports.WS_KEY_MAP.main2, exports.WS_KEY_MAP.main3, exports.WS_KEY_MAP.mainTestnetPublic, exports.WS_KEY_MAP.mainTestnetUserData, exports.WS_KEY_MAP.mainWSAPI, exports.WS_KEY_MAP.mainWSAPI2, exports.WS_KEY_MAP.mainWSAPITestnet, exports.WS_KEY_MAP.marginRiskUserData, ]; exports.WS_KEYS_FUTURES = [ exports.WS_KEY_MAP.usdm, exports.WS_KEY_MAP.usdmTestnet, exports.WS_KEY_MAP.usdmWSAPI, exports.WS_KEY_MAP.usdmWSAPITestnet, exports.WS_KEY_MAP.coinm, exports.WS_KEY_MAP.coinm2, exports.WS_KEY_MAP.coinmTestnet, exports.WS_KEY_MAP.coinmWSAPI, exports.WS_KEY_MAP.coinmWSAPITestnet, ]; exports.WS_KEY_URL_MAP = { // https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams main: 'wss://stream.binance.com:9443', // spot, margin, isolated margin, user data main2: 'wss://stream.binance.com:443', // spot, margin, isolated margin, user data | alternative main3: 'wss://data-stream.binance.vision', // spot, margin, isolated margin | alternative | MARKET DATA ONLY | NO USER DATA // https://developers.binance.com/docs/binance-spot-api-docs/testnet/web-socket-streams#general-wss-information mainTestnetPublic: 'wss://stream.testnet.binance.vision', mainTestnetUserData: 'wss://stream.testnet.binance.vision:9443', // https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/general-api-information mainWSAPI: 'wss://ws-api.binance.com:443', mainWSAPI2: 'wss://ws-api.binance.com:9443', mainWSAPITestnet: 'wss://ws-api.testnet.binance.vision', // https://developers.binance.com/docs/margin_trading/risk-data-stream // Margin websocket only support Cross Margin Accounts marginRiskUserData: 'wss://margin-stream.binance.com', // https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams // market data, user data usdm: 'wss://fstream.binance.com', // https://developers.binance.com/docs/derivatives/usds-margined-futures/general-info usdmTestnet: 'wss://stream.binancefuture.com', // https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-api-general-info // ONLY WS API // Suffix is handled in getWsURLSuffix usdmWSAPI: 'wss://ws-fapi.binance.com', usdmWSAPITestnet: 'wss://testnet.binancefuture.com', // https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams // market data, user data coinm: 'wss://dstream.binance.com', coinm2: 'wss://dstream-auth.binance.com', // Warning, coinm2 requires a listenkey // https://developers.binance.com/docs/derivatives/coin-margined-futures/general-info coinmTestnet: 'wss://dstream.binancefuture.com', // https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-api-general-info // ONLY WS API | NO USER DATA // Suffix is handled in getWsURLSuffix coinmWSAPI: 'wss://ws-dapi.binance.com', coinmWSAPITestnet: 'coinmWSAPITestnet', // https://developers.binance.com/docs/derivatives/option/websocket-market-streams // https://developers.binance.com/docs/derivatives/option/user-data-streams eoptions: 'wss://nbstream.binance.com/eoptions', // optionsTestnet: 'wss://testnetws.binanceops.com', // https://developers.binance.com/docs/derivatives/portfolio-margin/user-data-streams portfolioMarginUserData: 'wss://fstream.binance.com', // https://developers.binance.com/docs/derivatives/portfolio-margin-pro/portfolio-margin-pro-user-data-stream portfolioMarginProUserData: 'wss://fstream.binance.com', }; exports.WS_KEY_MM_URL_MAP = { // Spot connections - no MM endpoints main: undefined, main2: undefined, main3: undefined, mainTestnetPublic: undefined, mainTestnetUserData: undefined, mainWSAPI: undefined, mainWSAPI2: undefined, mainWSAPITestnet: undefined, marginRiskUserData: undefined, // USDM Futures MM endpoints usdm: 'wss://fstream-mm.binance.com', usdmTestnet: undefined, // No MM endpoint for testnet usdmWSAPI: 'wss://ws-fapi-mm.binance.com', usdmWSAPITestnet: undefined, // No MM endpoint for testnet // COINM Futures MM endpoints coinm: 'wss://dstream-mm.binance.com', coinm2: undefined, // No MM endpoint for coinm2 coinmTestnet: undefined, // No MM endpoint for testnet coinmWSAPI: 'wss://ws-dapi-mm.binance.com', coinmWSAPITestnet: undefined, // No MM endpoint for testnet // Other connections - no MM endpoints eoptions: undefined, portfolioMarginUserData: undefined, portfolioMarginProUserData: undefined, }; function getWsURLSuffix(wsKey, connectionType) { switch (wsKey) { case 'main': case 'main2': case 'main3': case 'marginRiskUserData': case 'mainTestnetPublic': case 'mainTestnetUserData': { switch (connectionType) { case 'market': return '/stream'; case 'userData': return '/ws'; default: { throw (0, typeGuards_1.neverGuard)(connectionType, `Unhandled connectionType "${wsKey}/${connectionType}"`); } } } case 'mainWSAPITestnet': case 'mainWSAPI': case 'mainWSAPI2': { return '/ws-api/v3'; } case 'usdm': case 'usdmTestnet': { switch (connectionType) { case 'market': return '/stream'; case 'userData': return '/ws'; default: { throw (0, typeGuards_1.neverGuard)(connectionType, `Unhandled connectionType "${wsKey}/${connectionType}"`); } } } case 'usdmWSAPI': case 'usdmWSAPITestnet': { return '/ws-fapi/v1'; } case 'coinmWSAPI': case 'coinmWSAPITestnet': { return '/ws-dapi/v1'; } case 'coinm': case 'coinmTestnet': case 'eoptions': switch (connectionType) { case 'market': return '/stream'; case 'userData': return '/ws'; default: { throw (0, typeGuards_1.neverGuard)(connectionType, `Unhandled connectionType "${wsKey}/${connectionType}"`); } } case 'coinm2': return '/stream&listenKey='; case 'portfolioMarginUserData': return '/pm/ws'; // pm/ws/listenKeyHere case 'portfolioMarginProUserData': return '/pm-classic/ws'; default: { throw (0, typeGuards_1.neverGuard)(wsKey, `Unhandled WsKey "${wsKey}"`); } } } exports.WS_AUTH_ON_CONNECT_KEYS = []; /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ const PRIVATE_TOPICS = []; function isPrivateWsTopic(topic) { return PRIVATE_TOPICS.includes(topic); } function getTestnetWsKey(wsKey) { switch (wsKey) { case exports.WS_KEY_MAP.mainTestnetPublic: case exports.WS_KEY_MAP.mainTestnetUserData: case exports.WS_KEY_MAP.coinmTestnet: case exports.WS_KEY_MAP.usdmTestnet: case exports.WS_KEY_MAP.mainWSAPITestnet: case exports.WS_KEY_MAP.usdmWSAPITestnet: case exports.WS_KEY_MAP.coinmWSAPITestnet: { return wsKey; } case exports.WS_KEY_MAP.main: case exports.WS_KEY_MAP.main2: case exports.WS_KEY_MAP.main3: { return exports.WS_KEY_MAP.mainTestnetUserData; } case exports.WS_KEY_MAP.mainWSAPI: case exports.WS_KEY_MAP.mainWSAPI2: { return exports.WS_KEY_MAP.mainWSAPITestnet; } case exports.WS_KEY_MAP.usdm: { return exports.WS_KEY_MAP.usdmTestnet; } case exports.WS_KEY_MAP.usdmWSAPI: { return exports.WS_KEY_MAP.usdmWSAPITestnet; } case exports.WS_KEY_MAP.coinm: case exports.WS_KEY_MAP.coinm2: { return exports.WS_KEY_MAP.coinmTestnet; } case exports.WS_KEY_MAP.coinmWSAPI: { return exports.WS_KEY_MAP.coinmWSAPITestnet; } case exports.WS_KEY_MAP.marginRiskUserData: case exports.WS_KEY_MAP.eoptions: case exports.WS_KEY_MAP.portfolioMarginUserData: case exports.WS_KEY_MAP.portfolioMarginProUserData: { throw new Error(`Testnet not supported for "${wsKey}"`); } default: throw (0, typeGuards_1.neverGuard)(wsKey, `Unhandled wsKey "${wsKey}"`); } } function getWsUrl(wsKey, wsClientOptions, logger) { const wsUrl = wsClientOptions.wsUrl; if (wsUrl) { return wsUrl; } const isTestnet = !!wsClientOptions.testnet; const useMMSubdomain = !!wsClientOptions.useMMSubdomain; const resolvedWsKey = isTestnet ? getTestnetWsKey(wsKey) : wsKey; // Use MM endpoints if requested and available if (useMMSubdomain && !isTestnet) { const mmUrl = exports.WS_KEY_MM_URL_MAP[resolvedWsKey]; if (mmUrl) { return mmUrl; } } return exports.WS_KEY_URL_MAP[resolvedWsKey]; } function getMaxTopicsPerSubscribeEvent(wsKey) { switch (wsKey) { // case 'v5': { // if (wsKey === WS_KEY_MAP.v5SpotPublic) { // return 10; // } // return null; // } default: { return 250; // throw neverGuard(wsKey, 'getMaxTopicsPerSubscribeEvent(): Unhandled wsKey'); } } } exports.WS_ERROR_ENUM = { NOT_AUTHENTICATED_SPOT_V3: '-1004', API_ERROR_GENERIC: '10001', API_SIGN_AUTH_FAILED: '10003', USDC_OPTION_AUTH_FAILED: '3303006', }; /** * #305: ws.terminate() is undefined in browsers. * This only works in node.js, not in browsers. * Does nothing if `ws` is undefined. Does nothing in browsers. */ function safeTerminateWs(ws, fallbackToClose) { if (!ws) { return false; } if (typeof ws['terminate'] === 'function') { ws.terminate(); return true; } else if (fallbackToClose) { ws.close(); } return false; } /** * WS API promises are stored using a primary key. This key is constructed using * properties found in every request & reply. * * The counterpart to this is in resolveEmittableEvents */ function getPromiseRefForWSAPIRequest(wsKey, requestEvent) { const promiseRef = [wsKey, requestEvent.id].join('_'); return promiseRef; } /** * Users can conveniently pass topics as strings or objects (object has topic name + optional params). * * This method normalises topics into objects (object has topic name + optional params). */ function getNormalisedTopicRequests(wsTopicRequests) { const normalisedTopicRequests = []; for (const wsTopicRequest of wsTopicRequests) { // passed as string, convert to object if (typeof wsTopicRequest === 'string') { const topicRequest = { topic: wsTopicRequest, payload: undefined, }; normalisedTopicRequests.push(topicRequest); continue; } // already a normalised object, thanks to user normalisedTopicRequests.push(wsTopicRequest); } return normalisedTopicRequests; } /** * Groups topics in request into per-wsKey groups * @param normalisedTopicRequests * @param wsKey * @param isPrivateTopic * @returns */ function getTopicsPerWSKey(normalisedTopicRequests, wsKey) { const perWsKeyTopics = {}; // Sort into per wsKey arrays, in case topics are mixed together for different wsKeys for (const topicRequest of normalisedTopicRequests) { const derivedWsKey = wsKey; if (!perWsKeyTopics[derivedWsKey] || !Array.isArray(perWsKeyTopics[derivedWsKey])) { perWsKeyTopics[derivedWsKey] = []; } perWsKeyTopics[derivedWsKey].push(topicRequest); } return perWsKeyTopics; } /** * Some of the newer multiplex websocket topics don't include an eventType ("e") property. * * This attempts to extract that from the streamName, which is included with these incoming events. */ function parseEventTypeFromMessage(wsKey, parsedMsg) { // console.log(`parseEventTypeFromMessage(${wsKey})`, parsedMsg); if (parsedMsg === null || parsedMsg === void 0 ? void 0 : parsedMsg.e) { return parsedMsg.e; } const streamName = (parsedMsg === null || parsedMsg === void 0 ? void 0 : parsedMsg.stream) || (parsedMsg === null || parsedMsg === void 0 ? void 0 : parsedMsg.streamName); if (streamName && typeof streamName === 'string') { // console.log(`parseEventTypeFromMessage(${wsKey}) `, streamName, parsedMsg); const eventType = streamName.split('@'); // All symbol streams can be returned as is, just need to extract the left-most text before any @ or _ if (streamName.startsWith('!')) { const subEventType = eventType[0].split('_'); return subEventType[0].replace('!', ''); } // Per symbol streams can have the symbol trimmed off (string before first "@") // E.g. btcusdt@kline_5m@+08:00 -> kline_5m@+08:00 if (eventType.length) { // remove first, keep the rest rejoined eventType.shift(); // Edge case, for european options, the suffix is a variable date so will never match the map if (wsKey === 'eoptions') { return eventType[0]; } return eventType.join('@'); } console.error('parseEventTypeFromMessage(): Cannot extract event type from message: ', parsedMsg); } if (parsedMsg === null || parsedMsg === void 0 ? void 0 : parsedMsg.data) { return parseEventTypeFromMessage(wsKey, parsedMsg.data); } if (Array.isArray(parsedMsg) && parsedMsg.length) { return parseEventTypeFromMessage(wsKey, parsedMsg[0]); } return; } function resolveUserDataMarketForWsKey(wsKey) { switch (wsKey) { case 'main': case 'main2': case 'main3': case 'mainWSAPI': case 'mainWSAPI2': case 'marginRiskUserData': return 'spot'; case 'mainTestnetPublic': case 'mainTestnetUserData': case 'mainWSAPITestnet': return 'spotTestnet'; case 'usdm': case 'usdmWSAPI': return 'usdm'; case 'usdmTestnet': case 'usdmWSAPITestnet': return 'usdmTestnet'; case 'coinm': case 'coinm2': case 'coinmWSAPI': return 'coinm'; case 'coinmTestnet': case 'coinmWSAPITestnet': return 'coinmTestnet'; case 'eoptions': return 'options'; case 'portfolioMarginUserData': case 'portfolioMarginProUserData': return 'portfoliom'; default: { throw (0, typeGuards_1.neverGuard)(wsKey, `resolveMarketForWsKey(): Unhandled WsKey "${wsKey}"`); } } } /** * Used by the legacy subscribe* utility methods to determine which wsKey to route the subscription to. */ function resolveWsKeyForLegacyMarket(market) { switch (market) { case 'spot': { return 'main'; } case 'coinm': { return 'coinm'; } case 'usdm': { return 'usdm'; } } } function parseRawWsMessageLegacy(event, options) { if (typeof event === 'string') { const parsedEvent = typeof options.customParseJSONFn === 'function' ? options.customParseJSONFn(event) : JSON.parse(event); // WS events are wrapped into "data" if (parsedEvent.data) { if (typeof parsedEvent.data === 'string') { return parseRawWsMessageLegacy(parsedEvent.data, options); } return parsedEvent.data; } // WS API wraps responses in "event" if (parsedEvent.event) { const { event } = parsedEvent, other = __rest(parsedEvent, ["event"]); return Object.assign(Object.assign({}, other), event); } return parsedEvent; } if (event === null || event === void 0 ? void 0 : event.data) { return parseRawWsMessageLegacy(event.data, options); } return event; } /** * One simple purpose - extract JSON event from raw WS Message. * * Any mapping or additonal handling should not be done here. */ function parseRawWsMessage(event, options) { // WS MessageLike->data (contains JSON as a string) if (event === null || event === void 0 ? void 0 : event.data) { return parseRawWsMessage(event.data, options); } if (typeof event === 'string') { // For: // - multiplex subscriptions, as of v3 // - user data, dedicated listen key connection // - user data, via ws api (Without listen key) // - ws api responses if (typeof options.customParseJSONFn === 'function') { return options.customParseJSONFn(event); } const parsedEvent = JSON.parse(event); return parsedEvent; } return event; } /** * @deprecated Only works for legacy WS client, where one connection exists per key */ function getContextFromWsKey(legacyWsKey) { const [market, streamName, symbol, listenKey, wsKey, ...otherParams] = legacyWsKey.split('_'); return { symbol: symbol === 'undefined' ? undefined : symbol, legacyWsKey, wsKey, market: market, isTestnet: market.includes('estnet'), isUserData: legacyWsKey.includes('userData'), streamName, listenKey: listenKey === 'undefined' ? undefined : listenKey, otherParams, }; } /** * The legacy WS client creates a deterministic WS Key based on consistent input parameters */ function getLegacyWsStoreKeyWithContext(market, streamName, symbol = undefined, listenKey = undefined, ...otherParams) { return [market, streamName, symbol, listenKey, ...otherParams].join('_'); } function getLegacyWsKeyContext(wsKey) { if (wsKey.indexOf('userData') !== -1) { return getContextFromWsKey(wsKey); } return undefined; } function getRealWsKeyFromDerivedWsKey(wsKey) { if (!wsKey.includes('userData')) { return wsKey; } const legacyWsKeyContext = getLegacyWsKeyContext(wsKey); if (!legacyWsKeyContext || !legacyWsKeyContext.wsKey) { throw new Error(`getRealWsKeyFromDerivedWsKey(): no context found in supplied wsKey: "${wsKey}" | "${legacyWsKeyContext}"`); } return legacyWsKeyContext.wsKey; } function appendEventMarket(wsMsg, wsKey) { const { market } = getContextFromWsKey(wsKey); wsMsg.wsMarket = market; wsMsg.wsKey = wsKey; } /** * WebSocket.ping() is not available in browsers. This is a simple check used to * disable heartbeats in browers, for exchanges that use native WebSocket ping/pong frames. */ function isWSPingFrameAvailable() { return typeof isomorphic_ws_1.default.prototype['ping'] === 'function'; } /** * WebSocket.pong() is not available in browsers. This is a simple check used to * disable heartbeats in browers, for exchanges that use native WebSocket ping/pong frames. */ function isWSPongFrameAvailable() { return typeof isomorphic_ws_1.default.prototype['pong'] === 'function'; } //# sourceMappingURL=websocket-util.js.map