@binance/common
Version:
Binance Common Types and Utilities for Binance Connectors
1,239 lines (1,233 loc) • 74 kB
JavaScript
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