UNPKG

@binance/common

Version:

Binance Common Types and Utilities for Binance Connectors

1,239 lines (1,233 loc) 74 kB
import crypto from "crypto"; import fs from "fs"; import https from "https"; import { JSONParse } from "json-with-bigint"; import { arch, platform } from "os"; import globalAxios from "axios"; import { EventEmitter } from "events"; import WebSocketClient from "ws"; //#region src/utils.ts /** * A weak cache for storing RequestSigner instances based on configuration parameters. * * @remarks * Uses a WeakMap to cache and reuse RequestSigner instances for configurations with * apiSecret, privateKey, and privateKeyPassphrase, allowing efficient memory management. */ let signerCache = /* @__PURE__ */ new WeakMap(); /** * Represents a request signer for generating signatures using HMAC-SHA256 or asymmetric key signing. * * Supports two signing methods: * 1. HMAC-SHA256 using an API secret * 2. Asymmetric signing using RSA or ED25519 private keys * * @throws {Error} If neither API secret nor private key is provided, or if the private key is invalid */ 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, bodyParams) { const params = buildQueryString(queryParams) + (bodyParams ? buildQueryString(bodyParams) : ""); 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."); } }; /** * Resets the signer cache to a new empty WeakMap. * * This function clears the existing signer cache, creating a fresh WeakMap * to store RequestSigner instances associated with configuration objects. */ const clearSignerCache = function() { signerCache = /* @__PURE__ */ new WeakMap(); }; /** * Serializes a value to a string representation. * * - If the value is `null` or `undefined`, returns an empty string. * - If the value is an array or a non-null object, returns its JSON string representation. * - Otherwise, converts the value to a string using `String()`. * * @param value - The value to serialize. * @returns The serialized string representation of the value. */ function serializeValue(value) { if (value === null || value === void 0) return ""; if (Array.isArray(value) || typeof value === "object" && value !== null) return JSON.stringify(value); return String(value); } /** * Builds a URL query string from the given parameters object. * * Iterates over the key-value pairs in the `params` object, serializes each value, * and encodes it for use in a URL. Only keys with non-null and non-undefined values * are included in the resulting query string. * * @param params - An object containing key-value pairs to be serialized into a query string. * @returns A URL-encoded query string representing the provided parameters. */ function buildQueryString(params) { if (!params) return ""; const pairs = []; Object.entries(params).forEach(([key, value]) => { if (value !== null && value !== void 0) { const serializedValue = serializeValue(value); pairs.push(`${key}=${encodeURIComponent(serializedValue)}`); } }); return pairs.join("&"); } /** * Generates a random string of 16 hexadecimal characters. * * @returns A random string of 16 hexadecimal characters. */ function randomString() { return crypto.randomBytes(16).toString("hex"); } /** * Generates a cryptographically secure random 32-bit unsigned integer. * * Uses the Web Crypto API to generate a random value between 0 and 4,294,967,295 (2^32 - 1). * * @returns A random 32-bit unsigned integer. */ function randomInteger() { const array = new Uint32Array(1); crypto.getRandomValues(array); return array[0]; } /** * Normalizes a stream ID to ensure it is valid, generating a random ID if needed. * * For string inputs: * - Returns the input if it's a valid 32-character hexadecimal string (case-insensitive) * - Otherwise, generates a new random hexadecimal string using `randomString()` * * For number inputs: * - Returns the input if it's a finite, non-negative integer within the safe integer range * - Otherwise, generates a new random integer using `randomInteger()` * * For null or undefined inputs: * - Generates a new random hexadecimal string using `randomString()` * * @param id - The stream ID to normalize (string, number, null, or undefined). * @param streamIdIsStrictlyNumber - Boolean forcing an id to be a number or not. * @returns A valid stream ID as either a 32-character hexadecimal string or a safe integer. */ function normalizeStreamId(id, streamIdIsStrictlyNumber) { const isValidNumber = typeof id === "number" && Number.isFinite(id) && Number.isInteger(id) && id >= 0 && id <= Number.MAX_SAFE_INTEGER; if (streamIdIsStrictlyNumber || typeof id === "number") return isValidNumber ? id : randomInteger(); if (typeof id === "string") return id && /^[0-9a-f]{32}$/i.test(id) ? id : randomString(); return randomString(); } /** * Validates the provided time unit string and returns it if it is either 'MILLISECOND' or 'MICROSECOND'. * * @param timeUnit - The time unit string to be validated. * @returns The validated time unit string, or `undefined` if the input is falsy. * @throws {Error} If the time unit is not 'MILLISECOND' or 'MICROSECOND'. */ 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; } /** * Delays the execution of the current function for the specified number of milliseconds. * * @param ms - The number of milliseconds to delay the function. * @returns A Promise that resolves after the specified delay. */ async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Generates the current timestamp in milliseconds. * * @returns The current timestamp in milliseconds. */ function getTimestamp() { return Date.now(); } /** * Generates a signature for the given configuration and query parameters using a cached request signer. * * @param configuration - Configuration object containing API secret, private key, and optional passphrase. * @param queryParams - The query parameters to be signed. * @returns A string representing the generated signature. */ const getSignature = function(configuration, queryParams, bodyParams) { let signer = signerCache.get(configuration); if (!signer) { signer = new RequestSigner(configuration); signerCache.set(configuration, signer); } return signer.sign(queryParams, bodyParams); }; /** * Asserts that a function parameter exists and is not null or undefined. * * @param functionName - The name of the function that the parameter belongs to. * @param paramName - The name of the parameter to check. * @param paramValue - The value of the parameter to check. * @throws {RequiredError} If the parameter is null or undefined. */ const 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}.`); }; /** * Sets the search parameters of a given URL object based on the provided key-value pairs. * Only parameters with non-null and non-undefined values are included. * Values are serialized using the `serializeValue` function before being set. * * @param url - The URL object whose search parameters will be updated. * @param params - An object containing key-value pairs to be set as search parameters. */ function setSearchParams(url, params) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== null && value !== void 0) { const serializedValue = serializeValue(value); searchParams.set(key, serializedValue); } }); url.search = searchParams.toString(); } /** * Converts a URL object to a full path string, including pathname, search parameters, and hash. * * @param url The URL object to convert to a path string. * @returns A complete path string representation of the URL. */ const toPathString = function(url) { return url.pathname + url.search + url.hash; }; /** * Normalizes scientific notation numbers in an object or array to a fixed number of decimal places. * * This function recursively processes objects, arrays, and numbers, converting scientific notation * to a fixed decimal representation. Non-numeric values are left unchanged. * * @template T The type of the input object or value * @param obj The object, array, or value to normalize * @returns A new object or value with scientific notation numbers normalized */ 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; } /** * Determines whether a request should be retried based on the provided error. * * This function checks the HTTP method, response status, and number of retries left to determine if a request should be retried. * * @param error The error object to check. * @param method The HTTP method of the request (optional). * @param retriesLeft The number of retries left (optional). * @returns `true` if the request should be retried, `false` otherwise. */ const 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); }; /** * Performs an HTTP request using the provided Axios instance and configuration. * * This function handles retries, rate limit handling, and error handling for the HTTP request. * * @param axiosArgs The request arguments to be passed to Axios. * @param configuration The configuration options for the request. * @returns A Promise that resolves to the API response, including the data and rate limit headers. */ const 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 JSONParse(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 = JSONParse(responseData); } catch { data = {}; } else if (typeof responseData === "object") data = responseData; } const errorMsg = data.msg; const errorCode = typeof data.code === "number" ? data.code : void 0; switch (status) { case 400: throw new BadRequestError(errorMsg, errorCode); case 401: throw new UnauthorizedError(errorMsg, errorCode); case 403: throw new ForbiddenError(errorMsg, errorCode); case 404: throw new NotFoundError(errorMsg, errorCode); case 418: throw new RateLimitBanError(errorMsg, errorCode); case 429: throw new TooManyRequestsError(errorMsg, errorCode); default: if (status >= 500 && status < 600) throw new ServerError(`Server error: ${status}`, status); throw new ConnectorClientError(errorMsg, errorCode); } } else { if (retries > 0 && attempt >= retries) lastError = /* @__PURE__ */ new Error(`Request failed after ${retries} retries`); else lastError = new NetworkError("Network error or request timeout."); break; } } throw lastError; }; /** * Parses the rate limit headers from the Axios response headers and returns an array of `RestApiRateLimit` objects. * * @param headers - The Axios response headers. * @returns An array of `RestApiRateLimit` objects containing the parsed rate limit information. */ const 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; }; /** * Generic function to send a request with optional API key and signature. * @param endpoint - The API endpoint to call. * @param method - HTTP method to use (GET, POST, DELETE, etc.). * @param params - Query parameters for the request. * @param timeUnit - The time unit for the request. * @param options - Additional request options (isSigned). * @returns A promise resolving to the response data object. */ const sendRequest = function(configuration, endpoint, method, queryParams = {}, bodyParams = {}, timeUnit, options = {}) { const localVarUrlObj = new URL(endpoint, configuration?.basePath); const localVarRequestOptions = { method, ...configuration?.baseOptions }; const localVarQueryParameter = { ...normalizeScientificNumbers(queryParams) }; const localVarBodyParameter = { ...normalizeScientificNumbers(bodyParams) }; if (options.isSigned) { localVarQueryParameter["timestamp"] = getTimestamp(); const signature = getSignature(configuration, localVarQueryParameter, localVarBodyParameter); if (signature) localVarQueryParameter["signature"] = signature; } setSearchParams(localVarUrlObj, localVarQueryParameter); if (Object.keys(localVarBodyParameter).length > 0) { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(localVarBodyParameter)) { if (value === null || value === void 0) continue; const serializedValue = serializeValue(value); searchParams.append(key, serializedValue); } localVarRequestOptions.data = searchParams.toString(); localVarRequestOptions.headers = { ...localVarRequestOptions.headers || {}, "Content-Type": "application/x-www-form-urlencoded" }; } 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); }; /** * Removes any null, undefined, or empty string values from the provided object. * * @param obj - The object to remove empty values from. * @returns A new object with empty values removed. */ function removeEmptyValue(obj) { if (!(obj instanceof Object)) return {}; return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== null && value !== void 0 && value !== "")); } /** * Sorts the properties of the provided object in alphabetical order and returns a new object with the sorted properties. * * @param obj - The object to be sorted. * @returns A new object with the properties sorted in alphabetical order. */ function sortObject(obj) { return Object.keys(obj).sort().reduce((res, key) => { res[key] = obj[key]; return res; }, {}); } /** * Replaces placeholders in the format <field> with corresponding values from the provided variables object. * * @param {string} str - The input string containing placeholders. * @param {Object} variables - An object where keys correspond to placeholder names and values are the replacements. * @returns {string} - The resulting string with placeholders replaced by their corresponding values. */ 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 ""; }); } /** * Generates a standardized user agent string for the application. * * @param {string} packageName - The name of the package/application. * @param {string} packageVersion - The version of the package/application. * @returns {string} A formatted user agent string including package details, Node.js version, platform, and architecture. */ function buildUserAgent(packageName, packageVersion) { return `${packageName}/${packageVersion} (Node.js/${process.version}; ${platform()}; ${arch()})`; } /** * Builds a WebSocket API message with optional authentication and signature. * * @param {ConfigurationWebsocketAPI} configuration - The WebSocket API configuration. * @param {string} method - The method name for the WebSocket message. * @param {WebsocketSendMsgOptions} payload - The payload data to be sent. * @param {WebsocketSendMsgConfig} options - Configuration options for message sending. * @param {boolean} [skipAuth=false] - Flag to skip authentication if needed. * @returns {Object} A structured WebSocket message with id, method, and params. */ 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 }; } /** * Sanitizes a header value by checking for and preventing carriage return and line feed characters. * * @param {string | string[]} value - The header value or array of header values to sanitize. * @returns {string | string[]} The sanitized header value(s). * @throws {Error} If the header value contains CR/LF characters. */ 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); } /** * Parses and sanitizes custom headers, filtering out forbidden headers. * * @param {Record<string, string | string[]>} headers - The input headers to be parsed. * @returns {Record<string, string | string[]>} A new object with sanitized and allowed headers. * @description Removes forbidden headers like 'host', 'authorization', and 'cookie', * and sanitizes remaining header values to prevent injection of carriage return or line feed characters. */ function parseCustomHeaders(headers) { if (!headers || Object.keys(headers).length === 0) return {}; const forbidden = 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; } //#endregion //#region 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, ...param.proxy.protocol && { protocol: param.proxy.protocol }, ...param.proxy.auth && { auth: param.proxy.auth } }, httpsAgent: param.httpsAgent ?? false, headers: { ...parseCustomHeaders(param.customHeaders || {}), "Content-Type": "application/json", "X-MBX-APIKEY": param.apiKey } }; } }; var ConfigurationWebsocketAPI = 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; } }; //#endregion //#region src/constants.ts const TimeUnit = { MILLISECOND: "MILLISECOND", millisecond: "millisecond", MICROSECOND: "MICROSECOND", microsecond: "microsecond" }; const ALGO_REST_API_PROD_URL = "https://api.binance.com"; const ALPHA_REST_API_PROD_URL = "https://www.binance.com"; const C2C_REST_API_PROD_URL = "https://api.binance.com"; const CONVERT_REST_API_PROD_URL = "https://api.binance.com"; const COPY_TRADING_REST_API_PROD_URL = "https://api.binance.com"; const CRYPTO_LOAN_REST_API_PROD_URL = "https://api.binance.com"; const DERIVATIVES_TRADING_COIN_FUTURES_REST_API_PROD_URL = "https://dapi.binance.com"; const DERIVATIVES_TRADING_COIN_FUTURES_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; const DERIVATIVES_TRADING_COIN_FUTURES_WS_API_PROD_URL = "wss://ws-dapi.binance.com/ws-dapi/v1"; const DERIVATIVES_TRADING_COIN_FUTURES_WS_API_TESTNET_URL = "wss://testnet.binancefuture.com/ws-dapi/v1"; const DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_PROD_URL = "wss://dstream.binance.com"; const DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_TESTNET_URL = "wss://dstream.binancefuture.com"; const DERIVATIVES_TRADING_USDS_FUTURES_REST_API_PROD_URL = "https://fapi.binance.com"; const DERIVATIVES_TRADING_USDS_FUTURES_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; const DERIVATIVES_TRADING_USDS_FUTURES_WS_API_PROD_URL = "wss://ws-fapi.binance.com/ws-fapi/v1"; const DERIVATIVES_TRADING_USDS_FUTURES_WS_API_TESTNET_URL = "wss://testnet.binancefuture.com/ws-fapi/v1"; const DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_PROD_URL = "wss://fstream.binance.com"; const DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_TESTNET_URL = "wss://stream.binancefuture.com"; const DERIVATIVES_TRADING_OPTIONS_REST_API_PROD_URL = "https://eapi.binance.com"; const DERIVATIVES_TRADING_OPTIONS_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; const DERIVATIVES_TRADING_OPTIONS_WS_STREAMS_PROD_URL = "wss://fstream.binance.com"; const DERIVATIVES_TRADING_OPTIONS_WS_STREAMS_TESTNET_URL = "wss://fstream.binancefuture.com"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_PROD_URL = "https://papi.binance.com"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_TESTNET_URL = "https://testnet.binancefuture.com"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_PROD_URL = "wss://fstream.binance.com/pm"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_TESTNET_URL = "wss://fstream.binancefuture.com/pm"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_REST_API_PROD_URL = "https://api.binance.com"; const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_WS_STREAMS_PROD_URL = "wss://fstream.binance.com/pm-classic"; const DUAL_INVESTMENT_REST_API_PROD_URL = "https://api.binance.com"; const FIAT_REST_API_PROD_URL = "https://api.binance.com"; const GIFT_CARD_REST_API_PROD_URL = "https://api.binance.com"; const MARGIN_TRADING_REST_API_PROD_URL = "https://api.binance.com"; const MARGIN_TRADING_WS_STREAMS_PROD_URL = "wss://stream.binance.com:9443"; const MARGIN_TRADING_RISK_WS_STREAMS_PROD_URL = "wss://margin-stream.binance.com"; const MINING_REST_API_PROD_URL = "https://api.binance.com"; const NFT_REST_API_PROD_URL = "https://api.binance.com"; const PAY_REST_API_PROD_URL = "https://api.binance.com"; const REBATE_REST_API_PROD_URL = "https://api.binance.com"; const SIMPLE_EARN_REST_API_PROD_URL = "https://api.binance.com"; const SPOT_REST_API_PROD_URL = "https://api.binance.com"; const SPOT_REST_API_TESTNET_URL = "https://testnet.binance.vision"; const SPOT_WS_API_PROD_URL = "wss://ws-api.binance.com:443/ws-api/v3"; const SPOT_WS_API_TESTNET_URL = "wss://ws-api.testnet.binance.vision/ws-api/v3"; const SPOT_WS_STREAMS_PROD_URL = "wss://stream.binance.com:9443"; const SPOT_WS_STREAMS_TESTNET_URL = "wss://stream.testnet.binance.vision"; const SPOT_REST_API_MARKET_URL = "https://data-api.binance.vision"; const SPOT_WS_STREAMS_MARKET_URL = "wss://data-stream.binance.vision"; const STAKING_REST_API_PROD_URL = "https://api.binance.com"; const SUB_ACCOUNT_REST_API_PROD_URL = "https://api.binance.com"; const VIP_LOAN_REST_API_PROD_URL = "https://api.binance.com"; const WALLET_REST_API_PROD_URL = "https://api.binance.com"; //#endregion //#region src/errors.ts /** * Represents an error that occurred in the Connector client. * @param msg - An optional error message. */ var ConnectorClientError = class ConnectorClientError extends Error { constructor(msg, code) { super(msg || "An unexpected error occurred."); Object.setPrototypeOf(this, ConnectorClientError.prototype); this.name = "ConnectorClientError"; this.code = code; } }; /** * Represents an error that occurs when a required parameter is missing or undefined. * @param field - The name of the missing parameter. * @param msg - An optional error message. */ 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"; } }; /** * Represents an error that occurs when a client is unauthorized to access a resource. * @param msg - An optional error message. */ var UnauthorizedError = class UnauthorizedError extends Error { constructor(msg, code) { super(msg || "Unauthorized access. Authentication required."); Object.setPrototypeOf(this, UnauthorizedError.prototype); this.name = "UnauthorizedError"; this.code = code; } }; /** * Represents an error that occurs when a resource is forbidden to the client. * @param msg - An optional error message. */ var ForbiddenError = class ForbiddenError extends Error { constructor(msg, code) { super(msg || "Access to the requested resource is forbidden."); Object.setPrototypeOf(this, ForbiddenError.prototype); this.name = "ForbiddenError"; this.code = code; } }; /** * Represents an error that occurs when client is doing too many requests. * @param msg - An optional error message. */ var TooManyRequestsError = class TooManyRequestsError extends Error { constructor(msg, code) { super(msg || "Too many requests. You are being rate-limited."); Object.setPrototypeOf(this, TooManyRequestsError.prototype); this.name = "TooManyRequestsError"; this.code = code; } }; /** * Represents an error that occurs when client's IP has been banned. * @param msg - An optional error message. */ var RateLimitBanError = class RateLimitBanError extends Error { constructor(msg, code) { super(msg || "The IP address has been banned for exceeding rate limits."); Object.setPrototypeOf(this, RateLimitBanError.prototype); this.name = "RateLimitBanError"; this.code = code; } }; /** * Represents an error that occurs when there is an internal server error. * @param msg - An optional error message. * @param statusCode - An optional HTTP status code associated with the error. */ 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"; } }; /** * Represents an error that occurs when a network error occurs. * @param msg - An optional error message. */ var NetworkError = class NetworkError extends Error { constructor(msg) { super(msg || "A network error occurred."); Object.setPrototypeOf(this, NetworkError.prototype); this.name = "NetworkError"; } }; /** * Represents an error that occurs when the requested resource was not found. * @param msg - An optional error message. */ var NotFoundError = class NotFoundError extends Error { constructor(msg, code) { super(msg || "The requested resource was not found."); Object.setPrototypeOf(this, NotFoundError.prototype); this.name = "NotFoundError"; this.code = code; } }; /** * Represents an error that occurs when a request is invalid or cannot be otherwise served. * @param msg - An optional error message. */ var BadRequestError = class BadRequestError extends Error { constructor(msg, code) { super(msg || "The request was invalid or cannot be otherwise served."); Object.setPrototypeOf(this, BadRequestError.prototype); this.name = "BadRequestError"; this.code = code; } }; //#endregion //#region src/logger.ts let LogLevel = /* @__PURE__ */ function(LogLevel$1) { LogLevel$1["NONE"] = ""; LogLevel$1["DEBUG"] = "debug"; LogLevel$1["INFO"] = "info"; LogLevel$1["WARN"] = "warn"; LogLevel$1["ERROR"] = "error"; return LogLevel$1; }({}); var Logger = class Logger { constructor() { this.minLogLevel = LogLevel.INFO; this.levelsOrder = [ LogLevel.NONE, LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR ]; const envLevel = process.env.LOG_LEVEL?.toLowerCase(); this.minLogLevel = envLevel && this.isValidLogLevel(envLevel) ? envLevel : LogLevel.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 === LogLevel.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}`); return this.levelsOrder.indexOf(level) >= this.levelsOrder.indexOf(this.minLogLevel); } debug(...message) { this.log(LogLevel.DEBUG, ...message); } info(...message) { this.log(LogLevel.INFO, ...message); } warn(...message) { this.log(LogLevel.WARN, ...message); } error(...message) { this.log(LogLevel.ERROR, ...message); } }; //#endregion //#region src/websocket.ts var WebsocketEventEmitter = class { constructor() { this.eventEmitter = new EventEmitter(); } on(event, listener) { this.eventEmitter.on(event, listener); } off(event, listener) { this.eventEmitter.off(event, listener); } emit(event, ...args) { this.eventEmitter.emit(event, ...args); } }; var WebsocketCommon = class WebsocketCommon extends WebsocketEventEmitter { static { this.MAX_CONNECTION_DURATION = 1380 * 60 * 1e3; } 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); } /** * 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. * @param urlPath - Optional URL path to filter connections. * @returns An array of available WebSocket connections. */ getAvailableConnections(allowNonEstablishedWebsockets = false, urlPath) { if (this.mode === "single" && !urlPath) return [this.connectionPool[0]]; return this.connectionPool.filter((connection) => this.isConnectionReady(connection, allowNonEstablishedWebsockets)); } /** * 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. * @param urlPath - An optional URL path to filter connections. * @returns {WebsocketConnection} The selected WebSocket connection. */ getConnection(allowNonEstablishedWebsockets = false, urlPath) { const availableConnections = this.getAvailableConnections(allowNonEstablishedWebsockets, urlPath).filter((connection) => { if (urlPath) return connection.urlPath === urlPath; return true; }); 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, delay$1, 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, delay$1); else timer = setInterval(callback, delay$1); 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}.`); await 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"); }); 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 { targetConnection.reconnectionPending = false; 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. */ getReconnectURL(defaultURL, targetConnection) { return defaultURL; } /** * Connects all WebSocket connections in the pool * @param url - The Websocket server URL. * @param connections - An optional array of WebSocket connections to connect. If not provided, all connections in the pool are connected. * @returns A promise that resolves when all connections are established. */ async connectPool(url, connections) { const connectPromises = (connections ?? this.connectionPool).map((connection) => new Promise((resolve, reject) => { this.initConnect(url, false, connection); connection.ws?.once("open", () => resolve()); connection.ws?.once("error", (err) => reject(err)); connection.ws?.once("close", () => reject(/* @__PURE__ */ 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.onOp