UNPKG

@binance/common

Version:

Binance Common Types and Utilities for Binance Connectors

1,317 lines (1,311 loc) 62.6 kB
// src/utils.ts import crypto from "crypto"; import fs from "fs"; import https from "https"; import { platform, arch } from "os"; import globalAxios from "axios"; var signerCache = /* @__PURE__ */ new WeakMap(); var RequestSigner = class { constructor(configuration) { if (configuration.apiSecret && !configuration.privateKey) { this.apiSecret = configuration.apiSecret; return; } if (configuration.privateKey) { let privateKey = configuration.privateKey; if (typeof privateKey === "string" && fs.existsSync(privateKey)) { privateKey = fs.readFileSync(privateKey, "utf-8"); } const keyInput = { key: privateKey }; if (configuration.privateKeyPassphrase && typeof configuration.privateKeyPassphrase === "string") { keyInput.passphrase = configuration.privateKeyPassphrase; } try { this.keyObject = crypto.createPrivateKey(keyInput); this.keyType = this.keyObject.asymmetricKeyType; } catch { throw new Error( "Invalid private key. Please provide a valid RSA or ED25519 private key." ); } return; } throw new Error("Either 'apiSecret' or 'privateKey' must be provided for signed requests."); } sign(queryParams) { const params = buildQueryString(queryParams); if (this.apiSecret) return crypto.createHmac("sha256", this.apiSecret).update(params).digest("hex"); if (this.keyObject && this.keyType) { const data = Buffer.from(params); if (this.keyType === "rsa") return crypto.sign("RSA-SHA256", data, this.keyObject).toString("base64"); if (this.keyType === "ed25519") return crypto.sign(null, data, this.keyObject).toString("base64"); throw new Error("Unsupported private key type. Must be RSA or ED25519."); } throw new Error("Signer is not properly initialized."); } }; var clearSignerCache = function() { signerCache = /* @__PURE__ */ new WeakMap(); }; function buildQueryString(params) { if (!params) return ""; return Object.entries(params).map(stringifyKeyValuePair).join("&"); } function stringifyKeyValuePair([key, value]) { const valueString = Array.isArray(value) ? `["${value.join('","')}"]` : value; return `${key}=${encodeURIComponent(valueString)}`; } function randomString() { return crypto.randomBytes(16).toString("hex"); } function validateTimeUnit(timeUnit) { if (!timeUnit) { return; } else if (timeUnit !== TimeUnit.MILLISECOND && timeUnit !== TimeUnit.MICROSECOND && timeUnit !== TimeUnit.millisecond && timeUnit !== TimeUnit.microsecond) { throw new Error("timeUnit must be either 'MILLISECOND' or 'MICROSECOND'"); } return timeUnit; } async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getTimestamp() { return Date.now(); } var getSignature = function(configuration, queryParams) { let signer = signerCache.get(configuration); if (!signer) { signer = new RequestSigner(configuration); signerCache.set(configuration, signer); } return signer.sign(queryParams); }; var assertParamExists = function(functionName, paramName, paramValue) { if (paramValue === null || paramValue === void 0) { throw new RequiredError( paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.` ); } }; function setFlattenedQueryParams(urlSearchParams, parameter, key = "") { if (parameter == null) return; if (Array.isArray(parameter)) { if (key) urlSearchParams.set(key, JSON.stringify(parameter)); else for (const item of parameter) { setFlattenedQueryParams(urlSearchParams, item, ""); } return; } if (typeof parameter === "object") { for (const subKey of Object.keys(parameter)) { const subVal = parameter[subKey]; const newKey = key ? `${key}.${subKey}` : subKey; setFlattenedQueryParams(urlSearchParams, subVal, newKey); } return; } const str = String(parameter); if (urlSearchParams.has(key)) urlSearchParams.append(key, str); else urlSearchParams.set(key, str); } var setSearchParams = function(url, ...objects) { const searchParams = new URLSearchParams(url.search); setFlattenedQueryParams(searchParams, objects); url.search = searchParams.toString(); }; var toPathString = function(url) { return url.pathname + url.search + url.hash; }; function normalizeScientificNumbers(obj) { if (Array.isArray(obj)) { return obj.map((item) => normalizeScientificNumbers(item)); } else if (typeof obj === "object" && obj !== null) { const result = {}; for (const key of Object.keys(obj)) { result[key] = normalizeScientificNumbers(obj[key]); } return result; } else if (typeof obj === "number") { if (!Number.isFinite(obj)) return obj; const abs = Math.abs(obj); if (abs === 0 || abs >= 1e-6 && abs < 1e21) return String(obj); const isNegative = obj < 0; const [rawMantissa, rawExponent] = abs.toExponential().split("e"); const exponent = +rawExponent; const digits = rawMantissa.replace(".", ""); if (exponent < 0) { const zeros = "0".repeat(Math.abs(exponent) - 1); return (isNegative ? "-" : "") + "0." + zeros + digits; } else { const pad = exponent - (digits.length - 1); if (pad >= 0) { return (isNegative ? "-" : "") + digits + "0".repeat(pad); } else { const point = digits.length + pad; return (isNegative ? "-" : "") + digits.slice(0, point) + "." + digits.slice(point); } } } else { return obj; } } var shouldRetryRequest = function(error, method, retriesLeft) { const isRetriableMethod = ["GET", "DELETE"].includes(method ?? ""); const isRetriableStatus = [500, 502, 503, 504].includes( error?.response?.status ?? 0 ); return (retriesLeft ?? 0) > 0 && isRetriableMethod && (isRetriableStatus || !error?.response); }; var httpRequestFunction = async function(axiosArgs, configuration) { const axiosRequestArgs = { ...axiosArgs.options, url: (globalAxios.defaults?.baseURL ? "" : configuration?.basePath ?? "") + axiosArgs.url }; if (configuration?.keepAlive && !configuration?.baseOptions?.httpsAgent) axiosRequestArgs.httpsAgent = new https.Agent({ keepAlive: true }); if (configuration?.compression) axiosRequestArgs.headers = { ...axiosRequestArgs.headers, "Accept-Encoding": "gzip, deflate, br" }; const retries = configuration?.retries ?? 0; const backoff = configuration?.backoff ?? 0; let attempt = 0; let lastError; while (attempt <= retries) { try { const response = await globalAxios.request({ ...axiosRequestArgs, responseType: "text" }); const rateLimits = parseRateLimitHeaders(response.headers); return { data: async () => { try { return JSON.parse(response.data); } catch (err) { throw new Error(`Failed to parse JSON response: ${err}`); } }, status: response.status, headers: response.headers, rateLimits }; } catch (error) { attempt++; const axiosError = error; if (shouldRetryRequest( axiosError, axiosRequestArgs?.method?.toUpperCase(), retries - attempt )) { await delay(backoff * attempt); } else { if (axiosError.response && axiosError.response.status) { const status = axiosError.response?.status; const responseData = axiosError.response.data; let data = {}; if (responseData && responseData !== null) { if (typeof responseData === "string" && responseData !== "") try { data = JSON.parse(responseData); } catch { data = {}; } else if (typeof responseData === "object") data = responseData; } const errorMsg = data.msg; switch (status) { case 400: throw new BadRequestError(errorMsg); case 401: throw new UnauthorizedError(errorMsg); case 403: throw new ForbiddenError(errorMsg); case 404: throw new NotFoundError(errorMsg); case 418: throw new RateLimitBanError(errorMsg); case 429: throw new TooManyRequestsError(errorMsg); default: if (status >= 500 && status < 600) throw new ServerError(`Server error: ${status}`, status); throw new ConnectorClientError(errorMsg); } } else { if (retries > 0 && attempt >= retries) lastError = new Error(`Request failed after ${retries} retries`); else lastError = new NetworkError("Network error or request timeout."); break; } } } } throw lastError; }; var parseRateLimitHeaders = function(headers) { const rateLimits = []; const parseIntervalDetails = (key) => { const match = key.match(/x-mbx-used-weight-(\d+)([smhd])|x-mbx-order-count-(\d+)([smhd])/i); if (!match) return null; const intervalNum = parseInt(match[1] || match[3], 10); const intervalLetter = (match[2] || match[4])?.toUpperCase(); let interval; switch (intervalLetter) { case "S": interval = "SECOND"; break; case "M": interval = "MINUTE"; break; case "H": interval = "HOUR"; break; case "D": interval = "DAY"; break; default: return null; } return { interval, intervalNum }; }; for (const [key, value] of Object.entries(headers)) { const normalizedKey = key.toLowerCase(); if (value === void 0) continue; if (normalizedKey.startsWith("x-mbx-used-weight-")) { const details = parseIntervalDetails(normalizedKey); if (details) { rateLimits.push({ rateLimitType: "REQUEST_WEIGHT", interval: details.interval, intervalNum: details.intervalNum, count: parseInt(value, 10) }); } } else if (normalizedKey.startsWith("x-mbx-order-count-")) { const details = parseIntervalDetails(normalizedKey); if (details) { rateLimits.push({ rateLimitType: "ORDERS", interval: details.interval, intervalNum: details.intervalNum, count: parseInt(value, 10) }); } } } if (headers["retry-after"]) { const retryAfter = parseInt(headers["retry-after"], 10); for (const limit of rateLimits) { limit.retryAfter = retryAfter; } } return rateLimits; }; var sendRequest = function(configuration, endpoint, method, params = {}, timeUnit, options = {}) { const localVarUrlObj = new URL(endpoint, configuration?.basePath); const localVarRequestOptions = { method, ...configuration?.baseOptions }; const localVarQueryParameter = { ...normalizeScientificNumbers(params) }; if (options.isSigned) { const timestamp = getTimestamp(); localVarQueryParameter["timestamp"] = timestamp; const signature = getSignature(configuration, localVarQueryParameter); if (signature) { localVarQueryParameter["signature"] = signature; } } setSearchParams(localVarUrlObj, localVarQueryParameter); if (timeUnit && localVarRequestOptions.headers) { const _timeUnit = validateTimeUnit(timeUnit); localVarRequestOptions.headers = { ...localVarRequestOptions.headers, "X-MBX-TIME-UNIT": _timeUnit }; } return httpRequestFunction( { url: toPathString(localVarUrlObj), options: localVarRequestOptions }, configuration ); }; function removeEmptyValue(obj) { if (!(obj instanceof Object)) return {}; return Object.fromEntries( Object.entries(obj).filter( ([, value]) => value !== null && value !== void 0 && value !== "" ) ); } function sortObject(obj) { return Object.keys(obj).sort().reduce((res, key) => { res[key] = obj[key]; return res; }, {}); } function replaceWebsocketStreamsPlaceholders(str, variables) { const normalizedVariables = Object.keys(variables).reduce( (acc, key) => { const normalizedKey = key.toLowerCase().replace(/[-_]/g, ""); acc[normalizedKey] = variables[key]; return acc; }, {} ); return str.replace(/(@)?<([^>]+)>/g, (match, precedingAt, fieldName) => { const normalizedFieldName = fieldName.toLowerCase().replace(/[-_]/g, ""); if (Object.prototype.hasOwnProperty.call(normalizedVariables, normalizedFieldName) && normalizedVariables[normalizedFieldName] != null) { const value = normalizedVariables[normalizedFieldName]; switch (normalizedFieldName) { case "symbol": case "windowsize": return value.toLowerCase(); case "updatespeed": return `@${value}`; default: return (precedingAt || "") + value; } } return ""; }); } function buildUserAgent(packageName, packageVersion) { return `${packageName}/${packageVersion} (Node.js/${process.version}; ${platform()}; ${arch()})`; } function buildWebsocketAPIMessage(configuration, method, payload, options, skipAuth = false) { const id = payload.id && /^[0-9a-f]{32}$/.test(payload.id) ? payload.id : randomString(); delete payload.id; let params = normalizeScientificNumbers(removeEmptyValue(payload)); if ((options.withApiKey || options.isSigned) && !skipAuth) params.apiKey = configuration.apiKey; if (options.isSigned) { params.timestamp = getTimestamp(); params = sortObject(params); if (!skipAuth) params.signature = getSignature(configuration, params); } return { id, method, params }; } function sanitizeHeaderValue(value) { const sanitizeOne = (v) => { if (/\r|\n/.test(v)) throw new Error(`Invalid header value (contains CR/LF): "${v}"`); return v; }; return Array.isArray(value) ? value.map(sanitizeOne) : sanitizeOne(value); } function parseCustomHeaders(headers) { if (!headers || Object.keys(headers).length === 0) return {}; const forbidden = /* @__PURE__ */ new Set(["host", "authorization", "cookie", ":method", ":path"]); const parsedHeaders = {}; for (const [rawName, rawValue] of Object.entries(headers || {})) { const name = rawName.trim(); if (forbidden.has(name.toLowerCase())) { Logger.getInstance().warn(`Dropping forbidden header: ${name}`); continue; } try { parsedHeaders[name] = sanitizeHeaderValue(rawValue); } catch { continue; } } return parsedHeaders; } // src/configuration.ts var ConfigurationRestAPI = class { constructor(param = { apiKey: "" }) { this.apiKey = param.apiKey; this.apiSecret = param.apiSecret; this.basePath = param.basePath; this.keepAlive = param.keepAlive ?? true; this.compression = param.compression ?? true; this.retries = param.retries ?? 3; this.backoff = param.backoff ?? 1e3; this.privateKey = param.privateKey; this.privateKeyPassphrase = param.privateKeyPassphrase; this.timeUnit = param.timeUnit; this.baseOptions = { timeout: param.timeout ?? 1e3, proxy: param.proxy && { host: param.proxy.host, port: param.proxy.port, auth: param.proxy.auth }, httpsAgent: param.httpsAgent ?? false, headers: { ...parseCustomHeaders(param.customHeaders || {}), "Content-Type": "application/json", "X-MBX-APIKEY": param.apiKey } }; } }; var ConfigurationWebsocketAPI2 = class { constructor(param = { apiKey: "" }) { this.apiKey = param.apiKey; this.apiSecret = param.apiSecret; this.wsURL = param.wsURL; this.timeout = param.timeout ?? 5e3; this.reconnectDelay = param.reconnectDelay ?? 5e3; this.compression = param.compression ?? true; this.agent = param.agent ?? false; this.mode = param.mode ?? "single"; this.poolSize = param.poolSize ?? 1; this.privateKey = param.privateKey; this.privateKeyPassphrase = param.privateKeyPassphrase; this.timeUnit = param.timeUnit; this.autoSessionReLogon = param.autoSessionReLogon ?? true; } }; var ConfigurationWebsocketStreams = class { constructor(param = {}) { this.wsURL = param.wsURL; this.reconnectDelay = param.reconnectDelay ?? 5e3; this.compression = param.compression ?? true; this.agent = param.agent ?? false; this.mode = param.mode ?? "single"; this.poolSize = param.poolSize ?? 1; this.timeUnit = param.timeUnit; } }; // src/constants.ts var TimeUnit = { MILLISECOND: "MILLISECOND", millisecond: "millisecond", MICROSECOND: "MICROSECOND", microsecond: "microsecond" }; var ALGO_REST_API_PROD_URL = "https://api.binance.com"; var AUTO_INVEST_REST_API_PROD_URL = "https://api.binance.com"; var C2C_REST_API_PROD_URL = "https://api.binance.com"; var CONVERT_REST_API_PROD_URL = "https://api.binance.com"; var COPY_TRADING_REST_API_PROD_URL = "https://api.binance.com"; var CRYPTO_LOAN_REST_API_PROD_URL = "https://api.binance.com"; var DERIVATIVES_TRADING_COIN_FUTURES_REST_API_PROD_URL = "https://dapi.binance.com"; var DERIVATIVES_TRADING_COIN_FUTURES_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; var DERIVATIVES_TRADING_COIN_FUTURES_WS_API_PROD_URL = "wss://ws-dapi.binance.com/ws-dapi/v1"; var DERIVATIVES_TRADING_COIN_FUTURES_WS_API_TESTNET_URL = "wss://testnet.binancefuture.com/ws-dapi/v1"; var DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_PROD_URL = "wss://dstream.binance.com"; var DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_TESTNET_URL = "wss://dstream.binancefuture.com"; var DERIVATIVES_TRADING_USDS_FUTURES_REST_API_PROD_URL = "https://fapi.binance.com"; var DERIVATIVES_TRADING_USDS_FUTURES_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; var DERIVATIVES_TRADING_USDS_FUTURES_WS_API_PROD_URL = "wss://ws-fapi.binance.com/ws-fapi/v1"; var DERIVATIVES_TRADING_USDS_FUTURES_WS_API_TESTNET_URL = "wss://testnet.binancefuture.com/ws-fapi/v1"; var DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_PROD_URL = "wss://fstream.binance.com"; var DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_TESTNET_URL = "wss://stream.binancefuture.com"; var DERIVATIVES_TRADING_OPTIONS_REST_API_PROD_URL = "https://eapi.binance.com"; var DERIVATIVES_TRADING_OPTIONS_WS_STREAMS_PROD_URL = "wss://nbstream.binance.com/eoptions"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_PROD_URL = "https://papi.binance.com"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_PROD_URL = "wss://fstream.binance.com/pm"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_TESTNET_URL = "wss://fstream.binancefuture.com/pm"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_REST_API_PROD_URL = "https://api.binance.com"; var DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_WS_STREAMS_PROD_URL = "wss://fstream.binance.com/pm-classic"; var DUAL_INVESTMENT_REST_API_PROD_URL = "https://api.binance.com"; var FIAT_REST_API_PROD_URL = "https://api.binance.com"; var GIFT_CARD_REST_API_PROD_URL = "https://api.binance.com"; var MARGIN_TRADING_REST_API_PROD_URL = "https://api.binance.com"; var MARGIN_TRADING_WS_STREAMS_PROD_URL = "wss://stream.binance.com:9443"; var MARGIN_TRADING_RISK_WS_STREAMS_PROD_URL = "wss://margin-stream.binance.com"; var MINING_REST_API_PROD_URL = "https://api.binance.com"; var NFT_REST_API_PROD_URL = "https://api.binance.com"; var PAY_REST_API_PROD_URL = "https://api.binance.com"; var REBATE_REST_API_PROD_URL = "https://api.binance.com"; var SIMPLE_EARN_REST_API_PROD_URL = "https://api.binance.com"; var SPOT_REST_API_PROD_URL = "https://api.binance.com"; var SPOT_REST_API_TESTNET_URL = "https://testnet.binance.vision"; var SPOT_WS_API_PROD_URL = "wss://ws-api.binance.com:443/ws-api/v3"; var SPOT_WS_API_TESTNET_URL = "wss://ws-api.testnet.binance.vision/ws-api/v3"; var SPOT_WS_STREAMS_PROD_URL = "wss://stream.binance.com:9443"; var SPOT_WS_STREAMS_TESTNET_URL = "wss://stream.testnet.binance.vision"; var SPOT_REST_API_MARKET_URL = "https://data-api.binance.vision"; var SPOT_WS_STREAMS_MARKET_URL = "wss://data-stream.binance.vision"; var STAKING_REST_API_PROD_URL = "https://api.binance.com"; var SUB_ACCOUNT_REST_API_PROD_URL = "https://api.binance.com"; var VIP_LOAN_REST_API_PROD_URL = "https://api.binance.com"; var WALLET_REST_API_PROD_URL = "https://api.binance.com"; // src/errors.ts var ConnectorClientError = class _ConnectorClientError extends Error { constructor(msg) { super(msg || "An unexpected error occurred."); Object.setPrototypeOf(this, _ConnectorClientError.prototype); this.name = "ConnectorClientError"; } }; var RequiredError = class _RequiredError extends Error { constructor(field, msg) { super(msg || `Required parameter ${field} was null or undefined.`); this.field = field; Object.setPrototypeOf(this, _RequiredError.prototype); this.name = "RequiredError"; } }; var UnauthorizedError = class _UnauthorizedError extends Error { constructor(msg) { super(msg || "Unauthorized access. Authentication required."); Object.setPrototypeOf(this, _UnauthorizedError.prototype); this.name = "UnauthorizedError"; } }; var ForbiddenError = class _ForbiddenError extends Error { constructor(msg) { super(msg || "Access to the requested resource is forbidden."); Object.setPrototypeOf(this, _ForbiddenError.prototype); this.name = "ForbiddenError"; } }; var TooManyRequestsError = class _TooManyRequestsError extends Error { constructor(msg) { super(msg || "Too many requests. You are being rate-limited."); Object.setPrototypeOf(this, _TooManyRequestsError.prototype); this.name = "TooManyRequestsError"; } }; var RateLimitBanError = class _RateLimitBanError extends Error { constructor(msg) { super(msg || "The IP address has been banned for exceeding rate limits."); Object.setPrototypeOf(this, _RateLimitBanError.prototype); this.name = "RateLimitBanError"; } }; var ServerError = class _ServerError extends Error { constructor(msg, statusCode) { super(msg || "An internal server error occurred."); this.statusCode = statusCode; Object.setPrototypeOf(this, _ServerError.prototype); this.name = "ServerError"; } }; var NetworkError = class _NetworkError extends Error { constructor(msg) { super(msg || "A network error occurred."); Object.setPrototypeOf(this, _NetworkError.prototype); this.name = "NetworkError"; } }; var NotFoundError = class _NotFoundError extends Error { constructor(msg) { super(msg || "The requested resource was not found."); Object.setPrototypeOf(this, _NotFoundError.prototype); this.name = "NotFoundError"; } }; var BadRequestError = class _BadRequestError extends Error { constructor(msg) { super(msg || "The request was invalid or cannot be otherwise served."); Object.setPrototypeOf(this, _BadRequestError.prototype); this.name = "BadRequestError"; } }; // src/logger.ts var LogLevel = /* @__PURE__ */ ((LogLevel2) => { LogLevel2["NONE"] = ""; LogLevel2["DEBUG"] = "debug"; LogLevel2["INFO"] = "info"; LogLevel2["WARN"] = "warn"; LogLevel2["ERROR"] = "error"; return LogLevel2; })(LogLevel || {}); var Logger = class _Logger { constructor() { this.minLogLevel = "info" /* INFO */; this.levelsOrder = [ "" /* NONE */, "debug" /* DEBUG */, "info" /* INFO */, "warn" /* WARN */, "error" /* ERROR */ ]; const envLevel = process.env.LOG_LEVEL?.toLowerCase(); this.minLogLevel = envLevel && this.isValidLogLevel(envLevel) ? envLevel : "info" /* INFO */; } static getInstance() { if (!_Logger.instance) _Logger.instance = new _Logger(); return _Logger.instance; } setMinLogLevel(level) { if (!this.isValidLogLevel(level)) throw new Error(`Invalid log level: ${level}`); this.minLogLevel = level; } isValidLogLevel(level) { return this.levelsOrder.includes(level); } log(level, ...message) { if (level === "" /* NONE */ || !this.allowLevelLog(level)) return; const timestamp = (/* @__PURE__ */ new Date()).toISOString(); console[level](`[${timestamp}] [${level.toLowerCase()}]`, ...message); } allowLevelLog(level) { if (!this.isValidLogLevel(level)) throw new Error(`Invalid log level: ${level}`); const currentLevelIndex = this.levelsOrder.indexOf(level); const minLevelIndex = this.levelsOrder.indexOf(this.minLogLevel); return currentLevelIndex >= minLevelIndex; } debug(...message) { this.log("debug" /* DEBUG */, ...message); } info(...message) { this.log("info" /* INFO */, ...message); } warn(...message) { this.log("warn" /* WARN */, ...message); } error(...message) { this.log("error" /* ERROR */, ...message); } }; // src/websocket.ts import { EventEmitter } from "events"; import WebSocketClient from "ws"; var WebsocketEventEmitter = class { constructor() { this.eventEmitter = new EventEmitter(); } /* eslint-disable @typescript-eslint/no-explicit-any */ on(event, listener) { this.eventEmitter.on(event, listener); } /* eslint-disable @typescript-eslint/no-explicit-any */ off(event, listener) { this.eventEmitter.off(event, listener); } /* eslint-disable @typescript-eslint/no-explicit-any */ emit(event, ...args) { this.eventEmitter.emit(event, ...args); } }; var WebsocketCommon = class _WebsocketCommon extends WebsocketEventEmitter { constructor(configuration, connectionPool = []) { super(); this.configuration = configuration; this.connectionQueue = []; this.queueProcessing = false; this.connectionTimers = /* @__PURE__ */ new Map(); this.roundRobinIndex = 0; this.logger = Logger.getInstance(); this.connectionPool = connectionPool; this.mode = this.configuration?.mode ?? "single"; this.poolSize = this.mode === "pool" && this.configuration?.poolSize ? this.configuration.poolSize : 1; if (!connectionPool || connectionPool.length === 0) this.initializePool(this.poolSize); } static { this.MAX_CONNECTION_DURATION = 23 * 60 * 60 * 1e3; } /** * Initializes the WebSocket connection pool by creating a specified number of connection objects * and adding them to the `connectionPool` array. Each connection object has the following properties: * - `closeInitiated`: a boolean indicating whether the connection has been closed * - `reconnectionPending`: a boolean indicating whether a reconnection is pending * - `pendingRequests`: a Map that tracks pending requests for the connection * @param size - The number of connection objects to create and add to the pool. * @returns void */ initializePool(size) { for (let i = 0; i < size; i++) { this.connectionPool.push({ id: randomString(), closeInitiated: false, reconnectionPending: false, renewalPending: false, pendingRequests: /* @__PURE__ */ new Map(), pendingSubscriptions: [] }); } } /** * Retrieves available WebSocket connections based on the connection mode and readiness. * In 'single' mode, returns the first connection in the pool. * In 'pool' mode, filters and returns connections that are ready for use. * @param allowNonEstablishedWebsockets - Optional flag to include non-established WebSocket connections. * @returns An array of available WebSocket connections. */ getAvailableConnections(allowNonEstablishedWebsockets = false) { if (this.mode === "single") return [this.connectionPool[0]]; const availableConnections = this.connectionPool.filter( (connection) => this.isConnectionReady(connection, allowNonEstablishedWebsockets) ); return availableConnections; } /** * Gets a WebSocket connection from the pool or single connection. * If the connection mode is 'single', it returns the first connection in the pool. * If the connection mode is 'pool', it returns an available connection from the pool, * using a round-robin selection strategy. If no available connections are found, it throws an error. * @param allowNonEstablishedWebsockets - A boolean indicating whether to allow connections that are not established. * @returns {WebsocketConnection} The selected WebSocket connection. */ getConnection(allowNonEstablishedWebsockets = false) { const availableConnections = this.getAvailableConnections(allowNonEstablishedWebsockets); if (availableConnections.length === 0) { throw new Error("No available Websocket connections are ready."); } const selectedConnection = availableConnections[this.roundRobinIndex % availableConnections.length]; this.roundRobinIndex = (this.roundRobinIndex + 1) % availableConnections.length; return selectedConnection; } /** * Checks if the provided WebSocket connection is ready for use. * A connection is considered ready if it is open, has no pending reconnection, and has not been closed. * @param connection - The WebSocket connection to check. * @param allowNonEstablishedWebsockets - An optional flag to allow non-established WebSocket connections. * @returns `true` if the connection is ready, `false` otherwise. */ isConnectionReady(connection, allowNonEstablishedWebsockets = false) { return (allowNonEstablishedWebsockets || connection.ws?.readyState === WebSocketClient.OPEN) && !connection.reconnectionPending && !connection.closeInitiated; } /** * Schedules a timer for a WebSocket connection and tracks it * @param connection WebSocket client instance * @param callback Function to execute when timer triggers * @param delay Time in milliseconds before callback execution * @param type Timer type ('timeout' or 'interval') * @returns Timer handle */ scheduleTimer(connection, callback, delay2, type = "timeout") { let timers = this.connectionTimers.get(connection); if (!timers) { timers = /* @__PURE__ */ new Set(); this.connectionTimers.set(connection, timers); } const timerRecord = { type }; const wrappedTimeout = () => { try { callback(); } finally { timers.delete(timerRecord); } }; let timer; if (type === "timeout") timer = setTimeout(wrappedTimeout, delay2); else timer = setInterval(callback, delay2); timerRecord.timer = timer; timers.add(timerRecord); return timer; } /** * Clears all timers associated with a WebSocket connection. * @param connection - The WebSocket client instance to clear timers for. * @returns void */ clearTimers(connection) { const timers = this.connectionTimers.get(connection); if (timers) { timers.forEach(({ timer, type }) => { if (type === "timeout") clearTimeout(timer); else if (type === "interval") clearInterval(timer); }); this.connectionTimers.delete(connection); } } /** * Processes the connection queue, reconnecting or renewing connections as needed. * This method is responsible for iterating through the connection queue and initiating * the reconnection or renewal process for each connection in the queue. It throttles * the queue processing to avoid overwhelming the server with too many connection * requests at once. * @param throttleRate - The time in milliseconds to wait between processing each * connection in the queue. * @returns A Promise that resolves when the queue has been fully processed. */ async processQueue(throttleRate = 1e3) { if (this.queueProcessing) return; this.queueProcessing = true; while (this.connectionQueue.length > 0) { const { connection, url, isRenewal } = this.connectionQueue.shift(); this.initConnect(url, isRenewal, connection); await delay(throttleRate); } this.queueProcessing = false; } /** * Enqueues a reconnection or renewal for a WebSocket connection. * This method adds the connection, URL, and renewal flag to the connection queue, * and then calls the `processQueue` method to initiate the reconnection or renewal * process. * @param connection - The WebSocket connection to reconnect or renew. * @param url - The URL to use for the reconnection or renewal. * @param isRenewal - A flag indicating whether this is a renewal (true) or a reconnection (false). */ enqueueReconnection(connection, url, isRenewal) { this.connectionQueue.push({ connection, url, isRenewal }); this.processQueue(); } /** * Gracefully closes a WebSocket connection after pending requests complete. * This method waits for any pending requests to complete before closing the connection. * It sets up a timeout to force-close the connection after 30 seconds if the pending requests * do not complete. Once all pending requests are completed, the connection is closed. * @param connectionToClose - The WebSocket client instance to close. * @param WebsocketConnectionToClose - The WebSocket connection to close. * @param connection - The WebSocket connection to close. * @returns Promise that resolves when the connection is closed. */ async closeConnectionGracefully(WebsocketConnectionToClose, connection) { if (!WebsocketConnectionToClose || !connection) return; this.logger.debug( `Waiting for pending requests to complete before disconnecting websocket on connection ${connection.id}.` ); const closePromise = new Promise((resolve) => { this.scheduleTimer( WebsocketConnectionToClose, () => { this.logger.warn( `Force-closing websocket connection after 30 seconds on connection ${connection.id}.` ); resolve(); }, 3e4 ); this.scheduleTimer( WebsocketConnectionToClose, () => { if (connection.pendingRequests.size === 0) { this.logger.debug( `All pending requests completed, closing websocket connection on connection ${connection.id}.` ); resolve(); } }, 1e3, "interval" ); }); await closePromise; this.logger.info(`Closing Websocket connection on connection ${connection.id}.`); WebsocketConnectionToClose.close(); this.cleanup(WebsocketConnectionToClose); } /** * Attempts to re-establish a session for a WebSocket connection. * If a session logon request exists and the connection is not already logged on, * it sends an authentication request and updates the connection's logged-on status. * @param connection - The WebSocket connection to re-authenticate. * @private */ async sessionReLogon(connection) { const req = connection.sessionLogonReq; if (req && !connection.isSessionLoggedOn) { const data = buildWebsocketAPIMessage( this.configuration, req.method, req.payload, req.options ); this.logger.debug(`Session re-logon on connection ${connection.id}`, data); try { await this.send( JSON.stringify(data), data.id, true, this.configuration.timeout, connection ); this.logger.debug( `Session re-logon on connection ${connection.id} was successful.` ); connection.isSessionLoggedOn = true; } catch (err) { this.logger.error(`Session re-logon on connection ${connection.id} failed:`, err); } } } /** * Cleans up WebSocket connection resources. * Removes all listeners and clears any associated timers for the provided WebSocket client. * @param ws - The WebSocket client to clean up. * @returns void */ cleanup(ws) { if (ws) { ws.removeAllListeners(); this.clearTimers(ws); } } /** * Handles incoming WebSocket messages * @param data Raw message data received * @param connection Websocket connection */ onMessage(data, connection) { this.emit("message", data.toString(), connection); } /** * Handles the opening of a WebSocket connection. * @param url - The URL of the WebSocket server. * @param targetConnection - The WebSocket connection being opened. * @param oldWSConnection - The WebSocket client instance associated with the old connection. */ onOpen(url, targetConnection, oldWSConnection) { this.logger.info( `Connected to the Websocket Server with id ${targetConnection.id}: ${url}` ); if (targetConnection.renewalPending) { targetConnection.renewalPending = false; this.closeConnectionGracefully(oldWSConnection, targetConnection); } else if (targetConnection.closeInitiated) { this.closeConnectionGracefully(targetConnection.ws, targetConnection); } else { this.emit("open", this); } this.sessionReLogon(targetConnection); } /** * Returns the URL to use when reconnecting. * Derived classes should override this to provide dynamic URLs. * @param defaultURL The URL originally passed during the first connection. * @param targetConnection The WebSocket connection being connected. * @returns The URL to reconnect to. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getReconnectURL(defaultURL, targetConnection) { return defaultURL; } /** * Connects all WebSocket connections in the pool * @param url - The Websocket server URL. * @returns A promise that resolves when all connections are established. */ async connectPool(url) { const connectPromises = this.connectionPool.map( (connection) => new Promise((resolve, reject) => { this.initConnect(url, false, connection); connection.ws?.on("open", () => resolve()); connection.ws?.on("error", (err) => reject(err)); connection.ws?.on( "close", () => reject(new Error("Connection closed unexpectedly.")) ); }) ); await Promise.all(connectPromises); } /** * Creates a new WebSocket client instance. * @param url - The URL to connect to. * @returns A new WebSocket client instance. */ createWebSocket(url) { const wsClientOptions = { perMessageDeflate: this.configuration?.compression, agent: this.configuration?.agent }; if (this.configuration.userAgent) wsClientOptions.headers = { "User-Agent": this.configuration.userAgent }; return new WebSocketClient(url, wsClientOptions); } /** * Initializes a WebSocket connection. * @param url - The Websocket server URL. * @param isRenewal - Whether this is a connection renewal. * @param connection - An optional WebSocket connection to use. * @returns The WebSocket connection. */ initConnect(url, isRenewal = false, connection) { const targetConnection = connection || this.getConnection(); if (targetConnection.renewalPending && isRenewal) { this.logger.warn( `Connection renewal with id ${targetConnection.id} is already in progress` ); return; } if (targetConnection.ws && targetConnection.ws.readyState === WebSocketClient.OPEN && !isRenewal) { this.logger.warn(`Connection with id ${targetConnection.id} already exists`); return; } const ws = this.createWebSocket(url); this.logger.info( `Establishing Websocket connection with id ${targetConnection.id} to: ${url}` ); if (isRenewal) targetConnection.renewalPending = true; else targetConnection.ws = ws; targetConnection.isSessionLoggedOn = false; this.scheduleTimer( ws, () => { this.logger.info(`Renewing Websocket connection with id ${targetConnection.id}`); targetConnection.isSessionLoggedOn = false; this.enqueueReconnection( targetConnection, this.getReconnectURL(url, targetConnection), true ); }, _WebsocketCommon.MAX_CONNECTION_DURATION ); ws.on("open", () => { const oldWSConnection = targetConnection.ws; if (targetConnection.renewalPending) targetConnection.ws = ws; this.onOpen(url, targetConnection, oldWSConnection); }); ws.on("message", (data) => { this.onMessage(data.toString(), targetConnection); }); ws.on("ping", () => { this.logger.debug("Received PING from server"); this.emit("ping"); ws.pong(); this.logger.debug("Responded PONG to server's PING message"); }); ws.on("pong", () => { this.logger.debug("Received PONG from server"); this.emit("pong"); }); ws.on("error", (err) => { this.logger.error("Received error from server"); this.logger.error(err); this.emit("error", err); }); ws.on("close", (closeEventCode, reason) => { this.emit("close", closeEventCode, reason); if (!targetConnection.closeInitiated && !isRenewal) { this.logger.warn( `Connection with id ${targetConnection.id} closed due to ${closeEventCode}: ${reason}` ); this.scheduleTimer( ws, () => { this.logger.info( `Reconnecting conection with id ${targetConnection.id} to the server.` ); targetConnection.isSessionLoggedOn = false; targetConnection.reconnectionPending = true; this.enqueueReconnection( targetConnection, this.getReconnectURL(url, targetConnection), false ); }, this.configuration?.reconnectDelay ?? 5e3 ); } }); return targetConnection; } /** * Checks if the WebSocket connection is currently open. * @param connection - An optional WebSocket connection to check. If not provided, the entire connection pool is checked. * @returns `true` if the connection is open, `false` otherwise. */ isConnected(connection) { const connectionPool = connection ? [connection] : this.connectionPool; return connectionPool.some((connection2) => this.isConnectionReady(connection2)); } /** * Disconnects from the WebSocket server. * If there is no active connection, a warning is logged. * Otherwise, all connections in the connection pool are closed gracefully, * and a message is logged indicating that the connection has been disconnected. * @returns A Promise that resolves when all connections have been closed. * @throws Error if the WebSocket client is not set. */ async disconnect() { if (!this.isConnected()) this.logger.warn("No connection to close."); else { this.connectionPool.forEach((connection) => { connection.closeInitiated = true; connection.isSessionLoggedOn = false; connection.sessionLogonReq = void 0; }); const disconnectPromises = this.connectionPool.map( (connection) => this.closeConnectionGracefully(connection.ws, connection) ); await Promise.all(disconnectPromises); this.logger.info("Disconnected with Binance Websocket Server"); } } /** * Sends a ping message to all connected Websocket servers in the pool. * If no connections are ready, a warning is logged. * For each active connection, the ping message is sent, and debug logs provide details. * @throws Error if a Websocket client is not set for a connection. */ pingServer() { const connectedConnections = this.connectionPool.filter( (connection) => this.isConnected(connection) ); if (connectedConnections.length === 0) { this.logger.warn("Ping only can be sent when connection is ready."); return; } this.logger.debug("Sending PING to all connected Websocket servers."); connectedConnections.forEach((connection) => { if (connection.ws) { connection.ws.ping(); this.logger.debug(`PING sent to connection with id ${connection.id}`); } else { this.logger.error("WebSocket Client not set for a connection."); } }); } /** * Sends a payload through the WebSocket connection. * @param payload - Message to send. * @param id - Optional request identifier. * @param promiseBased - Whether to return a promise. * @param timeout - Timeout duration in milliseconds. * @param connection - The WebSocket connection to use. * @returns A promise if `promiseBased` is true, void otherwise. * @throws Error if not connected or WebSocket client is not set. */ send(payload, id, promiseBased = true, timeout = 5e3, connection) { if (!this.isConnected(connection)) { const errorMsg = "Unable to send message \u2014 connection is not available."; this.logger.warn(errorMsg); if (promiseBased) return Promise.reject(new Error(errorMsg)); else throw new Error(errorMsg); } const connectionToUse = connection ?? this.getConnection(); if (!connectionToUse.ws) { const errorMsg = "Websocket Client not set"; this.logger.error(errorMsg); if (promiseBased) return Promise.reject(new Error(errorMsg)); else throw new Error(errorMsg); } connectionToUse.ws.send(payload); if (promiseBased) { return new Promise((resolve, reject) => { if (!id) return reject(new Error("id is required for promise-based sending.")); const timeoutHandle = setTimeout(() => { if (connectionToUse.pendingRequests.has(id)) { connectionToUse.pendingRequests.delete(id); reject(new Error(`Request timeout for id: ${id}`)); } }, timeout); connectionToUse.pendingRequests.set(id, { resolve: (v) => { clearTimeout(timeoutHandle); resolve(v); }, reject: (e) => { clearTimeout(timeoutHandle); reject(e); } }); }); } } }; var WebsocketAPIBase = class extends WebsocketCommon { constructor(configuration, connectionPool = []) { super(configuration, connectionPool); this.isConnecting = false; this.streamCallbackMap = /* @__PURE__ */ new Map(); this.logger = Logger.getInstance(); this.configuration = configuration; } /** * Prepares the WebSocket URL by adding optional timeUnit parameter * @param wsUrl The base WebSocket URL * @returns The formatted WebSocket URL with parameters */ prepareURL(wsUrl) { let url = wsUrl; if (this?.configuration.timeUnit) { try { const _timeUnit = validateTimeUnit(this.configuration.timeUnit); url = `${url}${url.includes("?") ? "&" : "?"}timeUnit=${_timeUnit}`; } catch (err) { this.logger.error(err); } } return url; } /** * Processes incoming WebSocket messages * @param data The raw message data received */ onMessage(data, connection) { try { const message = JSON.parse(data); const { id, status } = message; if (id && connection.pendingRequests.has(id)) { const request = connection.pendingRequests.get(id); connection.pendingRequests.delete(id); if (status && status >= 400) { request?.reject(message.error); } else { const response = { data: message.result ?? message.response, ...message.rateLimits && { rateLimits: message.rateLimits } }; request?.resolve(response); } } else if ("event" in message && "e" in message["event"] && this.streamCallbackMap.size > 0) { this.streamCallbackMap.forEach( (callbacks) => callbacks.forEach((callback) => callback(message["event"])) ); } else { this.logger.warn("Received response for unknown or timed-out request:", message); } } catch (error) { this.logger.error("Failed to parse WebSocket message:", data, error); } super.onMessage(data, connection); } /** * Establishes a WebSocket connection to Binance * @returns Promise that resolves when connection is established * @throws Error if connection times out */ connect() { if (this.isConnected()) { this.logger.info("WebSocket connection already established"); return Promise.resolve(); } return new Promise((resolve, reject) => { if (this.isConnecting) return; this.isConnecting = true; const timeout = setTimeout(() => { this.isConnecting = false; reject(new Error("Websocket connection timed out")); }, 1e4); this.connectPool(this.prepareURL(this.configuration.wsURL)).then(() => { this.isConnecting = false; resolve(); }).catch((error) => { this.isConnecting = false; reject(error); }).finally(() => { clearTimeout(timeout); }); }); } async sendMessage(method, payload = {}, options = {}) { if (!this.isConnected()) { throw new Error("Not connected"); } const isSessionReq = options.isSessionLogon || options.isSessionLogout; const connections = isSessionReq ? this.getAvailableConnections() : [this.getConnection()]; const skipAuth = isSessionReq ? false : this.configuration.autoSessionReLogon && connections[0].isSessionLoggedOn; const data = buildWebsocketAPIMessage( this.configuration, method, payload, options, skipAuth ); this.logger.debug("Send message to Binance WebSocket API Server:", data); const responses = await Promise.all( connections.map( (connection) => this.send( JSON.stringify(data), data.id, true, this.configuration.timeout, connection ) ) ); if (isSessionReq && this.configuration.autoSessionReLogon) { connections.forEach((connection) => { if (options.isSessionLogon) {