@binance/common
Version:
Binance Common Types and Utilities for Binance Connectors
1 lines • 130 kB
Source Map (JSON)
{"version":3,"sources":["../src/utils.ts","../src/configuration.ts","../src/constants.ts","../src/errors.ts","../src/logger.ts","../src/websocket.ts"],"sourcesContent":["import crypto from 'crypto';\nimport fs from 'fs';\nimport https from 'https';\nimport { platform, arch } from 'os';\nimport {\n AxiosResponseHeaders,\n RawAxiosResponseHeaders,\n AxiosResponse,\n AxiosError,\n RawAxiosRequestConfig,\n} from 'axios';\nimport globalAxios from 'axios';\nimport {\n type ConfigurationRestAPI,\n type RestApiRateLimit,\n type RestApiResponse,\n TimeUnit,\n RequiredError,\n BadRequestError,\n ConnectorClientError,\n ForbiddenError,\n NetworkError,\n NotFoundError,\n RateLimitBanError,\n ServerError,\n TooManyRequestsError,\n UnauthorizedError,\n AxiosRequestArgs,\n SendMessageOptions,\n ObjectType,\n WebsocketSendMsgOptions,\n WebsocketSendMsgConfig,\n ConfigurationWebsocketAPI,\n Logger,\n} from '.';\n\n/**\n * A weak cache for storing RequestSigner instances based on configuration parameters.\n *\n * @remarks\n * Uses a WeakMap to cache and reuse RequestSigner instances for configurations with\n * apiSecret, privateKey, and privateKeyPassphrase, allowing efficient memory management.\n */\nlet signerCache = new WeakMap<\n {\n apiSecret?: string;\n privateKey?: string | Buffer;\n privateKeyPassphrase?: string;\n },\n RequestSigner\n>();\n\n/**\n * Represents a request signer for generating signatures using HMAC-SHA256 or asymmetric key signing.\n *\n * Supports two signing methods:\n * 1. HMAC-SHA256 using an API secret\n * 2. Asymmetric signing using RSA or ED25519 private keys\n *\n * @throws {Error} If neither API secret nor private key is provided, or if the private key is invalid\n */\nclass RequestSigner {\n private apiSecret?: string;\n private keyObject?: crypto.KeyObject;\n private keyType?: string;\n\n constructor(configuration: {\n apiSecret?: string;\n privateKey?: string | Buffer;\n privateKeyPassphrase?: string;\n }) {\n // HMAC-SHA256 path\n if (configuration.apiSecret && !configuration.privateKey) {\n this.apiSecret = configuration.apiSecret;\n return;\n }\n\n // Asymmetric path\n if (configuration.privateKey) {\n let privateKey: string | Buffer = configuration.privateKey;\n\n // If path, read file once\n if (typeof privateKey === 'string' && fs.existsSync(privateKey)) {\n privateKey = fs.readFileSync(privateKey, 'utf-8');\n }\n\n // Build KeyObject once\n const keyInput: crypto.PrivateKeyInput = { key: privateKey };\n if (\n configuration.privateKeyPassphrase &&\n typeof configuration.privateKeyPassphrase === 'string'\n ) {\n keyInput.passphrase = configuration.privateKeyPassphrase;\n }\n\n try {\n this.keyObject = crypto.createPrivateKey(keyInput);\n this.keyType = this.keyObject.asymmetricKeyType;\n } catch {\n throw new Error(\n 'Invalid private key. Please provide a valid RSA or ED25519 private key.'\n );\n }\n\n return;\n }\n\n throw new Error('Either \\'apiSecret\\' or \\'privateKey\\' must be provided for signed requests.');\n }\n\n sign(queryParams: object): string {\n const params = buildQueryString(queryParams);\n\n // HMAC-SHA256 signing\n if (this.apiSecret)\n return crypto.createHmac('sha256', this.apiSecret).update(params).digest('hex');\n\n // Asymmetric signing\n if (this.keyObject && this.keyType) {\n const data = Buffer.from(params);\n\n if (this.keyType === 'rsa')\n return crypto.sign('RSA-SHA256', data, this.keyObject).toString('base64');\n if (this.keyType === 'ed25519')\n return crypto.sign(null, data, this.keyObject).toString('base64');\n\n throw new Error('Unsupported private key type. Must be RSA or ED25519.');\n }\n\n throw new Error('Signer is not properly initialized.');\n }\n}\n\n/**\n * Resets the signer cache to a new empty WeakMap.\n *\n * This function clears the existing signer cache, creating a fresh WeakMap\n * to store RequestSigner instances associated with configuration objects.\n */\nexport const clearSignerCache = function (): void {\n signerCache = new WeakMap<\n {\n apiSecret?: string;\n privateKey?: string | Buffer;\n privateKeyPassphrase?: string;\n },\n RequestSigner\n >();\n};\n\n/**\n * Generates a query string from an object of parameters.\n *\n * @param params - An object containing the query parameters.\n * @returns The generated query string.\n */\nexport function buildQueryString(params: object): string {\n if (!params) return '';\n return Object.entries(params).map(stringifyKeyValuePair).join('&');\n}\n\n/**\n * Converts a key-value pair into a URL-encoded query parameter string.\n *\n * @param [key, value] - The key-value pair to be converted.\n * @returns The URL-encoded query parameter string.\n */\nfunction stringifyKeyValuePair([key, value]: [string, string]) {\n const valueString = Array.isArray(value) ? `[\"${value.join('\",\"')}\"]` : value;\n return `${key}=${encodeURIComponent(valueString)}`;\n}\n\n/**\n * Generates a random string of 16 hexadecimal characters.\n *\n * @returns A random string of 16 hexadecimal characters.\n */\nexport function randomString() {\n return crypto.randomBytes(16).toString('hex');\n}\n\n/**\n * Validates the provided time unit string and returns it if it is either 'MILLISECOND' or 'MICROSECOND'.\n *\n * @param timeUnit - The time unit string to be validated.\n * @returns The validated time unit string, or `undefined` if the input is falsy.\n * @throws {Error} If the time unit is not 'MILLISECOND' or 'MICROSECOND'.\n */\nexport function validateTimeUnit(timeUnit: string): string | undefined {\n if (!timeUnit) {\n return;\n } else if (\n timeUnit !== TimeUnit.MILLISECOND &&\n timeUnit !== TimeUnit.MICROSECOND &&\n timeUnit !== TimeUnit.millisecond &&\n timeUnit !== TimeUnit.microsecond\n ) {\n throw new Error('timeUnit must be either \\'MILLISECOND\\' or \\'MICROSECOND\\'');\n }\n\n return timeUnit;\n}\n\n/**\n * Delays the execution of the current function for the specified number of milliseconds.\n *\n * @param ms - The number of milliseconds to delay the function.\n * @returns A Promise that resolves after the specified delay.\n */\nexport async function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Generates the current timestamp in milliseconds.\n *\n * @returns The current timestamp in milliseconds.\n */\nexport function getTimestamp(): number {\n return Date.now();\n}\n\n/**\n * Generates a signature for the given configuration and query parameters using a cached request signer.\n *\n * @param configuration - Configuration object containing API secret, private key, and optional passphrase.\n * @param queryParams - The query parameters to be signed.\n * @returns A string representing the generated signature.\n */\nexport const getSignature = function (\n configuration: {\n apiSecret?: string;\n privateKey?: string | Buffer;\n privateKeyPassphrase?: string;\n },\n queryParams: object\n): string {\n let signer = signerCache.get(configuration);\n if (!signer) {\n signer = new RequestSigner(configuration);\n signerCache.set(configuration, signer);\n }\n return signer.sign(queryParams);\n};\n\n/**\n * Asserts that a function parameter exists and is not null or undefined.\n *\n * @param functionName - The name of the function that the parameter belongs to.\n * @param paramName - The name of the parameter to check.\n * @param paramValue - The value of the parameter to check.\n * @throws {RequiredError} If the parameter is null or undefined.\n */\nexport const assertParamExists = function (\n functionName: string,\n paramName: string,\n paramValue: unknown\n) {\n if (paramValue === null || paramValue === undefined) {\n throw new RequiredError(\n paramName,\n `Required parameter ${paramName} was null or undefined when calling ${functionName}.`\n );\n }\n};\n\n/**\n * Recursively flattens an object or array into URL search parameters.\n *\n * This function handles nested objects and arrays by converting them into dot-notation query parameters.\n * It supports different types of parameters:\n * - Arrays can be stringified or recursively added\n * - Objects are flattened with dot notation keys\n * - Primitive values are converted to strings\n *\n * @param urlSearchParams The URLSearchParams object to modify\n * @param parameter The parameter to flatten (can be an object, array, or primitive)\n * @param key Optional key for nested parameters, used for creating dot-notation keys\n */\nexport function setFlattenedQueryParams(\n urlSearchParams: URLSearchParams,\n parameter: unknown,\n key: string = ''\n): void {\n if (parameter == null) return;\n\n // Array handling\n if (Array.isArray(parameter)) {\n if (key)\n // non-empty key: stringify the entire array\n urlSearchParams.set(key, JSON.stringify(parameter));\n else\n // empty key: recurse into each item without using an empty key\n for (const item of parameter) {\n setFlattenedQueryParams(urlSearchParams, item, '');\n }\n return;\n }\n\n // Object handling\n if (typeof parameter === 'object') {\n for (const subKey of Object.keys(parameter as Record<string, unknown>)) {\n const subVal = (parameter as Record<string, unknown>)[subKey];\n const newKey = key ? `${key}.${subKey}` : subKey;\n setFlattenedQueryParams(urlSearchParams, subVal, newKey);\n }\n return;\n }\n\n // Primitive handling\n const str = String(parameter);\n if (urlSearchParams.has(key)) urlSearchParams.append(key, str);\n else urlSearchParams.set(key, str);\n}\n\n/**\n * Sets the search parameters of the provided URL by flattening the given objects into the URL's search parameters.\n *\n * This function takes a URL and one or more objects, and updates the URL's search parameters by flattening the objects into key-value pairs. It uses the `setFlattenedQueryParams` function to recursively flatten the objects.\n *\n * @param url - The URL to update the search parameters for.\n * @param objects - One or more objects to flatten into the URL's search parameters.\n */\nexport const setSearchParams = function (url: URL, ...objects: Record<string, unknown>[]) {\n const searchParams = new URLSearchParams(url.search);\n setFlattenedQueryParams(searchParams, objects);\n url.search = searchParams.toString();\n};\n\n/**\n * Converts a URL object to a full path string, including pathname, search parameters, and hash.\n *\n * @param url The URL object to convert to a path string.\n * @returns A complete path string representation of the URL.\n */\nexport const toPathString = function (url: URL) {\n return url.pathname + url.search + url.hash;\n};\n\n/**\n * A type utility that transforms numbers in a type to their string representation when in scientific notation,\n * while preserving the structure of arrays and objects.\n *\n * @template T The input type to be transformed\n * @returns A type where numbers potentially become strings, maintaining the original type's structure\n */\ntype ScientificToString<T> = T extends number\n ? string | number\n : T extends Array<infer U>\n ? Array<ScientificToString<U>>\n : T extends object\n ? { [K in keyof T]: ScientificToString<T[K]> }\n : T;\n\n/**\n * Normalizes scientific notation numbers in an object or array to a fixed number of decimal places.\n *\n * This function recursively processes objects, arrays, and numbers, converting scientific notation\n * to a fixed decimal representation. Non-numeric values are left unchanged.\n *\n * @template T The type of the input object or value\n * @param obj The object, array, or value to normalize\n * @returns A new object or value with scientific notation numbers normalized\n */\nexport function normalizeScientificNumbers<T>(obj: T): ScientificToString<T> {\n if (Array.isArray(obj)) {\n return obj.map((item) => normalizeScientificNumbers(item)) as ScientificToString<T>;\n } else if (typeof obj === 'object' && obj !== null) {\n const result = {} as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n result[key] = normalizeScientificNumbers((obj as Record<string, unknown>)[key]);\n }\n return result as ScientificToString<T>;\n } else if (typeof obj === 'number') {\n if (!Number.isFinite(obj)) return obj as ScientificToString<T>;\n\n const abs = Math.abs(obj);\n if (abs === 0 || (abs >= 1e-6 && abs < 1e21)) return String(obj) as ScientificToString<T>;\n\n const isNegative = obj < 0;\n const [rawMantissa, rawExponent] = abs.toExponential().split('e');\n const exponent = +rawExponent;\n const digits = rawMantissa.replace('.', '');\n\n if (exponent < 0) {\n const zeros = '0'.repeat(Math.abs(exponent) - 1);\n return ((isNegative ? '-' : '') + '0.' + zeros + digits) as ScientificToString<T>;\n } else {\n const pad = exponent - (digits.length - 1);\n\n if (pad >= 0) {\n return ((isNegative ? '-' : '') +\n digits +\n '0'.repeat(pad)) as ScientificToString<T>;\n } else {\n const point = digits.length + pad;\n return ((isNegative ? '-' : '') +\n digits.slice(0, point) +\n '.' +\n digits.slice(point)) as ScientificToString<T>;\n }\n }\n } else {\n return obj as ScientificToString<T>;\n }\n}\n\n/**\n * Determines whether a request should be retried based on the provided error.\n *\n * This function checks the HTTP method, response status, and number of retries left to determine if a request should be retried.\n *\n * @param error The error object to check.\n * @param method The HTTP method of the request (optional).\n * @param retriesLeft The number of retries left (optional).\n * @returns `true` if the request should be retried, `false` otherwise.\n */\nexport const shouldRetryRequest = function (\n error: AxiosError | object,\n method?: string,\n retriesLeft?: number\n): boolean {\n const isRetriableMethod = ['GET', 'DELETE'].includes(method ?? '');\n const isRetriableStatus = [500, 502, 503, 504].includes(\n (error as AxiosError)?.response?.status ?? 0\n );\n return (\n (retriesLeft ?? 0) > 0 &&\n isRetriableMethod &&\n (isRetriableStatus || !(error as AxiosError)?.response)\n );\n};\n\n/**\n * Performs an HTTP request using the provided Axios instance and configuration.\n *\n * This function handles retries, rate limit handling, and error handling for the HTTP request.\n *\n * @param axiosArgs The request arguments to be passed to Axios.\n * @param configuration The configuration options for the request.\n * @returns A Promise that resolves to the API response, including the data and rate limit headers.\n */\nexport const httpRequestFunction = async function <T>(\n axiosArgs: AxiosRequestArgs,\n configuration?: ConfigurationRestAPI\n): Promise<RestApiResponse<T>> {\n const axiosRequestArgs = {\n ...axiosArgs.options,\n url: (globalAxios.defaults?.baseURL ? '' : (configuration?.basePath ?? '')) + axiosArgs.url,\n };\n\n if (configuration?.keepAlive && !configuration?.baseOptions?.httpsAgent)\n axiosRequestArgs.httpsAgent = new https.Agent({ keepAlive: true });\n\n if (configuration?.compression)\n axiosRequestArgs.headers = {\n ...axiosRequestArgs.headers,\n 'Accept-Encoding': 'gzip, deflate, br',\n };\n\n const retries = configuration?.retries ?? 0;\n const backoff = configuration?.backoff ?? 0;\n let attempt = 0;\n let lastError;\n\n while (attempt <= retries) {\n try {\n const response: AxiosResponse = await globalAxios.request({\n ...axiosRequestArgs,\n responseType: 'text',\n });\n const rateLimits: RestApiRateLimit[] = parseRateLimitHeaders(response.headers);\n return {\n data: async (): Promise<T> => {\n try {\n return JSON.parse(response.data) as T;\n } catch (err) {\n throw new Error(`Failed to parse JSON response: ${err}`);\n }\n },\n status: response.status,\n headers: response.headers as Record<string, string>,\n rateLimits,\n };\n } catch (error) {\n attempt++;\n const axiosError = error as AxiosError;\n\n if (\n shouldRetryRequest(\n axiosError,\n axiosRequestArgs?.method?.toUpperCase(),\n retries - attempt\n )\n ) {\n await delay(backoff * attempt);\n } else {\n if (axiosError.response && axiosError.response.status) {\n const status = axiosError.response?.status;\n const responseData = axiosError.response.data;\n\n let data: Record<string, unknown> = {};\n if (responseData && responseData !== null) {\n if (typeof responseData === 'string' && responseData !== '')\n try {\n data = JSON.parse(responseData);\n } catch {\n data = {};\n }\n else if (typeof responseData === 'object')\n data = responseData as Record<string, unknown>;\n }\n\n const errorMsg = (data as { msg?: string }).msg;\n\n switch (status) {\n case 400:\n throw new BadRequestError(errorMsg);\n case 401:\n throw new UnauthorizedError(errorMsg);\n case 403:\n throw new ForbiddenError(errorMsg);\n case 404:\n throw new NotFoundError(errorMsg);\n case 418:\n throw new RateLimitBanError(errorMsg);\n case 429:\n throw new TooManyRequestsError(errorMsg);\n default:\n if (status >= 500 && status < 600)\n throw new ServerError(`Server error: ${status}`, status);\n throw new ConnectorClientError(errorMsg);\n }\n } else {\n if (retries > 0 && attempt >= retries)\n lastError = new Error(`Request failed after ${retries} retries`);\n else lastError = new NetworkError('Network error or request timeout.');\n\n break;\n }\n }\n }\n }\n\n throw lastError;\n};\n\n/**\n * Parses the rate limit headers from the Axios response headers and returns an array of `RestApiRateLimit` objects.\n *\n * @param headers - The Axios response headers.\n * @returns An array of `RestApiRateLimit` objects containing the parsed rate limit information.\n */\nexport const parseRateLimitHeaders = function (\n headers: RawAxiosResponseHeaders | AxiosResponseHeaders\n): RestApiRateLimit[] {\n const rateLimits: RestApiRateLimit[] = [];\n\n const parseIntervalDetails = (\n key: string\n ): { interval: 'SECOND' | 'MINUTE' | 'HOUR' | 'DAY'; intervalNum: number } | null => {\n const match = key.match(/x-mbx-used-weight-(\\d+)([smhd])|x-mbx-order-count-(\\d+)([smhd])/i);\n if (!match) return null;\n\n const intervalNum = parseInt(match[1] || match[3], 10);\n const intervalLetter = (match[2] || match[4])?.toUpperCase();\n\n let interval: 'SECOND' | 'MINUTE' | 'HOUR' | 'DAY';\n switch (intervalLetter) {\n case 'S':\n interval = 'SECOND';\n break;\n case 'M':\n interval = 'MINUTE';\n break;\n case 'H':\n interval = 'HOUR';\n break;\n case 'D':\n interval = 'DAY';\n break;\n default:\n return null;\n }\n\n return { interval, intervalNum };\n };\n\n for (const [key, value] of Object.entries(headers)) {\n const normalizedKey = key.toLowerCase();\n if (value === undefined) continue;\n\n if (normalizedKey.startsWith('x-mbx-used-weight-')) {\n const details = parseIntervalDetails(normalizedKey);\n if (details) {\n rateLimits.push({\n rateLimitType: 'REQUEST_WEIGHT',\n interval: details.interval,\n intervalNum: details.intervalNum,\n count: parseInt(value, 10),\n });\n }\n } else if (normalizedKey.startsWith('x-mbx-order-count-')) {\n const details = parseIntervalDetails(normalizedKey);\n if (details) {\n rateLimits.push({\n rateLimitType: 'ORDERS',\n interval: details.interval,\n intervalNum: details.intervalNum,\n count: parseInt(value, 10),\n });\n }\n }\n }\n\n if (headers['retry-after']) {\n const retryAfter = parseInt(headers['retry-after'], 10);\n for (const limit of rateLimits) {\n limit.retryAfter = retryAfter;\n }\n }\n\n return rateLimits;\n};\n\n/**\n * Generic function to send a request with optional API key and signature.\n * @param endpoint - The API endpoint to call.\n * @param method - HTTP method to use (GET, POST, DELETE, etc.).\n * @param params - Query parameters for the request.\n * @param timeUnit - The time unit for the request.\n * @param options - Additional request options (isSigned).\n * @returns A promise resolving to the response data object.\n */\nexport const sendRequest = function <T>(\n configuration: ConfigurationRestAPI,\n endpoint: string,\n method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH',\n params: Record<string, unknown> = {},\n timeUnit?: TimeUnit,\n options: { isSigned?: boolean } = {}\n): Promise<RestApiResponse<T>> {\n const localVarUrlObj = new URL(endpoint, configuration?.basePath);\n const localVarRequestOptions: RawAxiosRequestConfig = {\n method,\n ...configuration?.baseOptions,\n };\n const localVarQueryParameter = { ...normalizeScientificNumbers(params) };\n\n if (options.isSigned) {\n const timestamp = getTimestamp();\n localVarQueryParameter['timestamp'] = timestamp;\n const signature = getSignature(configuration!, localVarQueryParameter);\n if (signature) {\n localVarQueryParameter['signature'] = signature;\n }\n }\n\n setSearchParams(localVarUrlObj, localVarQueryParameter);\n\n if (timeUnit && localVarRequestOptions.headers) {\n const _timeUnit = validateTimeUnit(timeUnit);\n localVarRequestOptions.headers = {\n ...localVarRequestOptions.headers,\n 'X-MBX-TIME-UNIT': _timeUnit,\n };\n }\n\n return httpRequestFunction<T>(\n {\n url: toPathString(localVarUrlObj),\n options: localVarRequestOptions,\n },\n configuration\n );\n};\n\n/**\n * Removes any null, undefined, or empty string values from the provided object.\n *\n * @param obj - The object to remove empty values from.\n * @returns A new object with empty values removed.\n */\nexport function removeEmptyValue(obj: object): SendMessageOptions {\n if (!(obj instanceof Object)) return {};\n return Object.fromEntries(\n Object.entries(obj).filter(\n ([, value]) => value !== null && value !== undefined && value !== ''\n )\n );\n}\n\n/**\n * Sorts the properties of the provided object in alphabetical order and returns a new object with the sorted properties.\n *\n * @param obj - The object to be sorted.\n * @returns A new object with the properties sorted in alphabetical order.\n */\nexport function sortObject(obj: ObjectType) {\n return Object.keys(obj)\n .sort()\n .reduce((res: ObjectType, key: string) => {\n res[key] = obj[key] as string | number | boolean | object;\n return res;\n }, {});\n}\n\n/**\n * Replaces placeholders in the format <field> with corresponding values from the provided variables object.\n *\n * @param {string} str - The input string containing placeholders.\n * @param {Object} variables - An object where keys correspond to placeholder names and values are the replacements.\n * @returns {string} - The resulting string with placeholders replaced by their corresponding values.\n */\nexport function replaceWebsocketStreamsPlaceholders(\n str: string,\n variables: Record<string, unknown>\n): string {\n const normalizedVariables = Object.keys(variables).reduce(\n (acc, key) => {\n const normalizedKey = key.toLowerCase().replace(/[-_]/g, '');\n acc[normalizedKey] = variables[key];\n return acc;\n },\n {} as Record<string, unknown>\n );\n\n return str.replace(/(@)?<([^>]+)>/g, (match, precedingAt, fieldName) => {\n const normalizedFieldName = fieldName.toLowerCase().replace(/[-_]/g, '');\n\n if (\n Object.prototype.hasOwnProperty.call(normalizedVariables, normalizedFieldName) &&\n normalizedVariables[normalizedFieldName] != null\n ) {\n const value = normalizedVariables[normalizedFieldName];\n\n switch (normalizedFieldName) {\n case 'symbol':\n case 'windowsize':\n return (value as string).toLowerCase();\n case 'updatespeed':\n return `@${value}`;\n default:\n return (precedingAt || '') + (value as string);\n }\n }\n\n return '';\n });\n}\n\n/**\n * Generates a standardized user agent string for the application.\n *\n * @param {string} packageName - The name of the package/application.\n * @param {string} packageVersion - The version of the package/application.\n * @returns {string} A formatted user agent string including package details, Node.js version, platform, and architecture.\n */\nexport function buildUserAgent(packageName: string, packageVersion: string): string {\n return `${packageName}/${packageVersion} (Node.js/${process.version}; ${platform()}; ${arch()})`;\n}\n\n/**\n * Builds a WebSocket API message with optional authentication and signature.\n *\n * @param {ConfigurationWebsocketAPI} configuration - The WebSocket API configuration.\n * @param {string} method - The method name for the WebSocket message.\n * @param {WebsocketSendMsgOptions} payload - The payload data to be sent.\n * @param {WebsocketSendMsgConfig} options - Configuration options for message sending.\n * @param {boolean} [skipAuth=false] - Flag to skip authentication if needed.\n * @returns {Object} A structured WebSocket message with id, method, and params.\n */\nexport function buildWebsocketAPIMessage(\n configuration: ConfigurationWebsocketAPI,\n method: string,\n payload: WebsocketSendMsgOptions,\n options: WebsocketSendMsgConfig,\n skipAuth: boolean = false\n): { id: string; method: string; params: Record<string, unknown> } {\n const id = payload.id && /^[0-9a-f]{32}$/.test(payload.id) ? payload.id : randomString();\n delete payload.id;\n\n let params = normalizeScientificNumbers(removeEmptyValue(payload));\n if ((options.withApiKey || options.isSigned) && !skipAuth) params.apiKey = configuration.apiKey;\n\n if (options.isSigned) {\n params.timestamp = getTimestamp();\n params = sortObject(params as ObjectType);\n if (!skipAuth) params.signature = getSignature(configuration!, params);\n }\n\n return { id, method, params };\n}\n\n/**\n * Sanitizes a header value by checking for and preventing carriage return and line feed characters.\n *\n * @param {string | string[]} value - The header value or array of header values to sanitize.\n * @returns {string | string[]} The sanitized header value(s).\n * @throws {Error} If the header value contains CR/LF characters.\n */\nexport function sanitizeHeaderValue(value: string | string[]): string | string[] {\n const sanitizeOne = (v: string) => {\n if (/\\r|\\n/.test(v)) throw new Error(`Invalid header value (contains CR/LF): \"${v}\"`);\n return v;\n };\n\n return Array.isArray(value) ? value.map(sanitizeOne) : sanitizeOne(value);\n}\n\n/**\n * Parses and sanitizes custom headers, filtering out forbidden headers.\n *\n * @param {Record<string, string | string[]>} headers - The input headers to be parsed.\n * @returns {Record<string, string | string[]>} A new object with sanitized and allowed headers.\n * @description Removes forbidden headers like 'host', 'authorization', and 'cookie',\n * and sanitizes remaining header values to prevent injection of carriage return or line feed characters.\n */\nexport function parseCustomHeaders(\n headers: Record<string, string | string[]>\n): Record<string, string | string[]> {\n if (!headers || Object.keys(headers).length === 0) return {};\n\n const forbidden = new Set(['host', 'authorization', 'cookie', ':method', ':path']);\n const parsedHeaders: Record<string, string | string[]> = {};\n\n for (const [rawName, rawValue] of Object.entries(headers || {})) {\n const name = rawName.trim();\n if (forbidden.has(name.toLowerCase())) {\n Logger.getInstance().warn(`Dropping forbidden header: ${name}`);\n continue;\n }\n\n try {\n parsedHeaders[name] = sanitizeHeaderValue(rawValue);\n } catch {\n continue;\n }\n }\n\n return parsedHeaders;\n}\n","import { Agent } from 'https';\nimport type { TimeUnit } from './constants';\nimport { parseCustomHeaders } from './utils';\n\nexport class ConfigurationRestAPI {\n /**\n * The API key used for authentication.\n * @memberof ConfigurationRestAPI\n */\n apiKey: string;\n /**\n * The API secret used for authentication.\n * @memberof ConfigurationRestAPI\n */\n apiSecret?: string;\n /**\n * override base path\n * @type {string}\n * @memberof ConfigurationRestAPI\n */\n basePath?: string;\n /**\n * set a timeout (in milliseconds) for the request\n * @default 1000\n * @type {number}\n * @memberof ConfigurationRestAPI\n */\n timeout?: number;\n /**\n * HTTP/HTTPS proxy configuration\n * @default false\n * @type {object}\n * @property {string} host - Proxy server hostname\n * @property {number} port - Proxy server port number\n * @property {string} protocol - Proxy server protocol\n * @property {object} [auth] - Proxy authentication credentials\n * @property {string} auth.username - Proxy authentication username\n * @property {string} auth.password - Proxy authentication password\n * @memberof ConfigurationRestAPI\n */\n proxy?: {\n host: string;\n port: number;\n protocol?: string;\n auth?: { username: string; password: string };\n };\n /**\n * Optional custom headers to be sent with the request\n * @default {}\n * @type {Record<string, string | string[]>}\n * @memberof ConfigurationRestAPI\n */\n customHeaders?: Record<string, string | string[]>;\n /**\n * enables keep-alive functionality for the connection (if httpsAgent is set then we use httpsAgent.keepAlive instead)\n * @default true\n * @type {boolean}\n * @memberof ConfigurationRestAPI\n */\n keepAlive?: boolean;\n /**\n * enables response compression\n * @default true\n * @type {boolean}\n * @memberof ConfigurationRestAPI\n */\n compression?: boolean;\n /**\n * number of retry attempts for failed requests\n * @default 3\n * @type {number}\n * @memberof ConfigurationRestAPI\n */\n retries?: number;\n /**\n * delay between retry attempts in milliseconds\n * @default 1000\n * @type {number}\n * @memberof ConfigurationRestAPI\n */\n backoff?: number;\n /**\n * https agent\n * @default false\n * @type {boolean | Agent}\n * @memberof ConfigurationRestAPI\n */\n httpsAgent?: boolean | Agent;\n /**\n * private key\n * @type {string | Buffer}\n * @memberof ConfigurationRestAPI\n */\n privateKey?: string | Buffer;\n /**\n * private key passphrase\n * @type {string}\n * @memberof ConfigurationRestAPI\n */\n privateKeyPassphrase?: string;\n /**\n * timeUnit (used only on SPOT API)\n * @type {TimeUnit}\n * @memberof ConfigurationRestAPI\n */\n timeUnit?: TimeUnit;\n /**\n * base options for axios calls\n * @type {Record<string, unknown>}\n * @memberof ConfigurationRestAPI\n * @internal\n */\n baseOptions?: Record<string, unknown>;\n\n constructor(param: ConfigurationRestAPI = { apiKey: '' }) {\n this.apiKey = param.apiKey;\n this.apiSecret = param.apiSecret;\n this.basePath = param.basePath;\n this.keepAlive = param.keepAlive ?? true;\n this.compression = param.compression ?? true;\n this.retries = param.retries ?? 3;\n this.backoff = param.backoff ?? 1000;\n this.privateKey = param.privateKey;\n this.privateKeyPassphrase = param.privateKeyPassphrase;\n this.timeUnit = param.timeUnit;\n this.baseOptions = {\n timeout: param.timeout ?? 1000,\n proxy: param.proxy && {\n host: param.proxy.host,\n port: param.proxy.port,\n auth: param.proxy.auth,\n },\n httpsAgent: param.httpsAgent ?? false,\n headers: {\n ...parseCustomHeaders(param.customHeaders || {}),\n 'Content-Type': 'application/json',\n 'X-MBX-APIKEY': param.apiKey,\n },\n };\n }\n}\n\nexport class ConfigurationWebsocketAPI {\n /**\n * The API key used for authentication.\n * @memberof ConfigurationWebsocketAPI\n */\n apiKey: string;\n /**\n * The API secret used for authentication.\n * @memberof ConfigurationWebsocketAPI\n */\n apiSecret?: string;\n /**\n * override websocket url\n * @type {string}\n * @memberof ConfigurationWebsocketAPI\n */\n wsURL?: string;\n /**\n * set a timeout (in milliseconds) for the request\n * @default 5000\n * @type {number}\n * @memberof ConfigurationWebsocketAPI\n */\n timeout?: number;\n /**\n * reconnction delay\n * @default 5000\n * @type {number}\n * @memberof ConfigurationWebsocketAPI\n */\n reconnectDelay?: number;\n /**\n * use compression for websocket messages\n * @default true\n * @type {boolean}\n * @memberof ConfigurationWebsocketAPI\n */\n compression?: boolean;\n /**\n * websocket agent\n * @default false\n * @type {boolean | Agent}\n * @memberof ConfigurationWebsocketAPI\n */\n agent?: boolean | Agent;\n /**\n * the mode of the connection, either 'single' or 'pool'.\n * @default 'single'\n * @type {'single' | 'pool'}\n * @memberof ConfigurationWebsocketAPI\n */\n mode?: 'single' | 'pool';\n /**\n * the size of the connection pool, if the mode is set to 'pool'.\n * @default 1\n * @type {number}\n * @memberof ConfigurationWebsocketAPI\n */\n poolSize?: number;\n /**\n * private key\n * @type {string | Buffer}\n * @memberof ConfigurationWebsocketAPI\n */\n privateKey?: string | Buffer;\n /**\n * private key passphrase\n * @type {string}\n * @memberof ConfigurationWebsocketAPI\n */\n privateKeyPassphrase?: string;\n /**\n * timeUnit (used only on SPOT API)\n * @type {TimeUnit}\n * @memberof ConfigurationWebsocketAPI\n */\n timeUnit?: TimeUnit;\n /**\n * auto session re-logon on reconnects/renewals\n * @default true\n * @type {boolean}\n * @memberof ConfigurationWebsocketAPI\n */\n autoSessionReLogon?: boolean;\n /**\n * Optional user agent string for identifying the client\n * @type {string}\n * @memberof ConfigurationWebsocketStreams\n * @internal\n */\n userAgent?: string;\n\n constructor(param: ConfigurationWebsocketAPI = { apiKey: '' }) {\n this.apiKey = param.apiKey;\n this.apiSecret = param.apiSecret;\n this.wsURL = param.wsURL;\n this.timeout = param.timeout ?? 5000;\n this.reconnectDelay = param.reconnectDelay ?? 5000;\n this.compression = param.compression ?? true;\n this.agent = param.agent ?? false;\n this.mode = param.mode ?? 'single';\n this.poolSize = param.poolSize ?? 1;\n this.privateKey = param.privateKey;\n this.privateKeyPassphrase = param.privateKeyPassphrase;\n this.timeUnit = param.timeUnit;\n this.autoSessionReLogon = param.autoSessionReLogon ?? true;\n }\n}\n\nexport class ConfigurationWebsocketStreams {\n /**\n * override websocket url\n * @type {string}\n * @memberof ConfigurationWebsocketStreams\n */\n wsURL?: string;\n /**\n * reconnction delay\n * @default 5000\n * @type {number}\n * @memberof ConfigurationWebsocketStreams\n */\n reconnectDelay?: number;\n /**\n * use compression for websocket messages\n * @default true\n * @type {boolean}\n * @memberof ConfigurationWebsocketAPI\n */\n compression?: boolean;\n /**\n * websocket agent\n * @default false\n * @type {boolean | Agent}\n * @memberof ConfigurationWebsocketStreams\n */\n agent?: boolean | Agent;\n /**\n * the mode of the connection, either 'single' or 'pool'.\n * @default single\n * @type {'single' | 'pool'}\n * @memberof ConfigurationWebsocketStreams\n */\n mode?: 'single' | 'pool';\n /**\n * the size of the connection pool, if the mode is set to 'pool'.\n * @default 1\n * @type {number}\n * @memberof ConfigurationWebsocketStreams\n */\n poolSize?: number;\n /**\n * timeUnit (used only on SPOT API)\n * @type {TimeUnit}\n * @memberof ConfigurationWebsocketStreams\n */\n timeUnit?: TimeUnit;\n /**\n * Optional user agent string for identifying the client\n * @type {string}\n * @memberof ConfigurationWebsocketStreams\n * @internal\n */\n userAgent?: string;\n\n constructor(param: ConfigurationWebsocketStreams = {}) {\n this.wsURL = param.wsURL;\n this.reconnectDelay = param.reconnectDelay ?? 5000;\n this.compression = param.compression ?? true;\n this.agent = param.agent ?? false;\n this.mode = param.mode ?? 'single';\n this.poolSize = param.poolSize ?? 1;\n this.timeUnit = param.timeUnit;\n }\n}\n","export const TimeUnit = {\n MILLISECOND: 'MILLISECOND',\n millisecond: 'millisecond',\n MICROSECOND: 'MICROSECOND',\n microsecond: 'microsecond',\n} as const;\nexport type TimeUnit = (typeof TimeUnit)[keyof typeof TimeUnit];\n\n// Algo constants\nexport const ALGO_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Auto Invest constants\nexport const AUTO_INVEST_REST_API_PROD_URL = 'https://api.binance.com';\n\n// C2C constants\nexport const C2C_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Convert constants\nexport const CONVERT_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Copy Trading constants\nexport const COPY_TRADING_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Crypto Loan constants\nexport const CRYPTO_LOAN_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Derivatives Trading (COIN-M Futures) constants\nexport const DERIVATIVES_TRADING_COIN_FUTURES_REST_API_PROD_URL = 'https://dapi.binance.com';\nexport const DERIVATIVES_TRADING_COIN_FUTURES_REST_API_TESTNET_URL =\n 'https://testnet.binancefuture.com';\nexport const DERIVATIVES_TRADING_COIN_FUTURES_WS_API_PROD_URL =\n 'wss://ws-dapi.binance.com/ws-dapi/v1';\nexport const DERIVATIVES_TRADING_COIN_FUTURES_WS_API_TESTNET_URL =\n 'wss://testnet.binancefuture.com/ws-dapi/v1';\nexport const DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_PROD_URL = 'wss://dstream.binance.com';\nexport const DERIVATIVES_TRADING_COIN_FUTURES_WS_STREAMS_TESTNET_URL =\n 'wss://dstream.binancefuture.com';\n\n// Derivatives Trading (USDS Futures) constants\nexport const DERIVATIVES_TRADING_USDS_FUTURES_REST_API_PROD_URL = 'https://fapi.binance.com';\nexport const DERIVATIVES_TRADING_USDS_FUTURES_REST_API_TESTNET_URL =\n 'https://testnet.binancefuture.com';\nexport const DERIVATIVES_TRADING_USDS_FUTURES_WS_API_PROD_URL =\n 'wss://ws-fapi.binance.com/ws-fapi/v1';\nexport const DERIVATIVES_TRADING_USDS_FUTURES_WS_API_TESTNET_URL =\n 'wss://testnet.binancefuture.com/ws-fapi/v1';\nexport const DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_PROD_URL = 'wss://fstream.binance.com';\nexport const DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_TESTNET_URL =\n 'wss://stream.binancefuture.com';\n\n// Derivatives Trading (Options) constants\nexport const DERIVATIVES_TRADING_OPTIONS_REST_API_PROD_URL = 'https://eapi.binance.com';\nexport const DERIVATIVES_TRADING_OPTIONS_WS_STREAMS_PROD_URL =\n 'wss://nbstream.binance.com/eoptions';\n\n// Derivatives Trading (Portfolio Margin) constants\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_PROD_URL = 'https://papi.binance.com';\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_REST_API_TESTNET_URL =\n 'https://testnet.binancefuture.com';\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_PROD_URL =\n 'wss://fstream.binance.com/pm';\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_WS_STREAMS_TESTNET_URL =\n 'wss://fstream.binancefuture.com/pm';\n\n// Derivatives Trading (Portfolio Margin Pro) constants\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_REST_API_PROD_URL = 'https://api.binance.com';\nexport const DERIVATIVES_TRADING_PORTFOLIO_MARGIN_PRO_WS_STREAMS_PROD_URL =\n 'wss://fstream.binance.com/pm-classic';\n\n// Dual Investment constants\nexport const DUAL_INVESTMENT_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Fiat constants\nexport const FIAT_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Gift Card constants\nexport const GIFT_CARD_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Margin Trading constants\nexport const MARGIN_TRADING_REST_API_PROD_URL = 'https://api.binance.com';\nexport const MARGIN_TRADING_WS_STREAMS_PROD_URL = 'wss://stream.binance.com:9443';\nexport const MARGIN_TRADING_RISK_WS_STREAMS_PROD_URL = 'wss://margin-stream.binance.com';\n\n// Mining constants\nexport const MINING_REST_API_PROD_URL = 'https://api.binance.com';\n\n// NFT constants\nexport const NFT_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Pay constants\nexport const PAY_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Rebate constants\nexport const REBATE_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Simple Earn constants\nexport const SIMPLE_EARN_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Spot constants\nexport const SPOT_REST_API_PROD_URL = 'https://api.binance.com';\nexport const SPOT_REST_API_TESTNET_URL = 'https://testnet.binance.vision';\nexport const SPOT_WS_API_PROD_URL = 'wss://ws-api.binance.com:443/ws-api/v3';\nexport const SPOT_WS_API_TESTNET_URL = 'wss://ws-api.testnet.binance.vision/ws-api/v3';\nexport const SPOT_WS_STREAMS_PROD_URL = 'wss://stream.binance.com:9443';\nexport const SPOT_WS_STREAMS_TESTNET_URL = 'wss://stream.testnet.binance.vision';\nexport const SPOT_REST_API_MARKET_URL = 'https://data-api.binance.vision';\nexport const SPOT_WS_STREAMS_MARKET_URL = 'wss://data-stream.binance.vision';\n\n// Staking constants\nexport const STAKING_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Sub Account constants\nexport const SUB_ACCOUNT_REST_API_PROD_URL = 'https://api.binance.com';\n\n// VIP Loan constants\nexport const VIP_LOAN_REST_API_PROD_URL = 'https://api.binance.com';\n\n// Wallet constants\nexport const WALLET_REST_API_PROD_URL = 'https://api.binance.com';\n","/**\n * Represents an error that occurred in the Connector client.\n * @param msg - An optional error message.\n */\nexport class ConnectorClientError extends Error {\n constructor(msg?: string) {\n super(msg || 'An unexpected error occurred.');\n Object.setPrototypeOf(this, ConnectorClientError.prototype);\n this.name = 'ConnectorClientError';\n }\n}\n\n/**\n * Represents an error that occurs when a required parameter is missing or undefined.\n * @param field - The name of the missing parameter.\n * @param msg - An optional error message.\n */\nexport class RequiredError extends Error {\n constructor(\n public field: string,\n msg?: string\n ) {\n super(msg || `Required parameter ${field} was null or undefined.`);\n Object.setPrototypeOf(this, RequiredError.prototype);\n this.name = 'RequiredError';\n }\n}\n\n/**\n * Represents an error that occurs when a client is unauthorized to access a resource.\n * @param msg - An optional error message.\n */\nexport class UnauthorizedError extends Error {\n constructor(msg?: string) {\n super(msg || 'Unauthorized access. Authentication required.');\n Object.setPrototypeOf(this, UnauthorizedError.prototype);\n this.name = 'UnauthorizedError';\n }\n}\n\n/**\n * Represents an error that occurs when a resource is forbidden to the client.\n * @param msg - An optional error message.\n */\nexport class ForbiddenError extends Error {\n constructor(msg?: string) {\n super(msg || 'Access to the requested resource is forbidden.');\n Object.setPrototypeOf(this, ForbiddenError.prototype);\n this.name = 'ForbiddenError';\n }\n}\n\n/**\n * Represents an error that occurs when client is doing too many requests.\n * @param msg - An optional error message.\n */\nexport class TooManyRequestsError extends Error {\n constructor(msg?: string) {\n super(msg || 'Too many requests. You are being rate-limited.');\n Object.setPrototypeOf(this, TooManyRequestsError.prototype);\n this.name = 'TooManyRequestsError';\n }\n}\n\n/**\n * Represents an error that occurs when client's IP has been banned.\n * @param msg - An optional error message.\n */\nexport class RateLimitBanError extends Error {\n constructor(msg?: string) {\n super(msg || 'The IP address has been banned for exceeding rate limits.');\n Object.setPrototypeOf(this, RateLimitBanError.prototype);\n this.name = 'RateLimitBanError';\n }\n}\n\n/**\n * Represents an error that occurs when there is an internal server error.\n * @param msg - An optional error message.\n * @param statusCode - An optional HTTP status code associated with the error.\n */\nexport class ServerError extends Error {\n constructor(\n msg?: string,\n public statusCode?: number\n ) {\n super(msg || 'An internal server error occurred.');\n Object.setPrototypeOf(this, ServerError.prototype);\n this.name = 'ServerError';\n }\n}\n\n/**\n * Represents an error that occurs when a network error occurs.\n * @param msg - An optional error message.\n */\nexport class NetworkError extends Error {\n constructor(msg?: string) {\n super(msg || 'A network error occurred.');\n Object.setPrototypeOf(this, NetworkError.prototype);\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Represents an error that occurs when the requested resource was not found.\n * @param msg - An optional error message.\n */\nexport class NotFoundError extends Error {\n constructor(msg?: string) {\n super(msg || 'The requested resource was not found.');\n Object.setPrototypeOf(this, NotFoundError.prototype);\n this.name = 'NotFoundError';\n }\n}\n\n/**\n * Represents an error that occurs when a request is invalid or cannot be otherwise served.\n * @param msg - An optional error message.\n */\nexport class BadRequestError extends Error {\n constructor(msg?: string) {\n super(msg || 'The request was invalid or cannot be otherwise served.');\n Object.setPrototypeOf(this, BadRequestError.prototype);\n this.name = 'BadRequestError';\n }\n}\n","export enum LogLevel {\n NONE = '',\n DEBUG = 'debug',\n INFO = 'info',\n WARN = 'warn',\n E