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