ccxt
Version:
949 lines (944 loc) • 450 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var secp256k1_js = require('@noble/curves/secp256k1.js');
var sha3_js = require('@noble/hashes/sha3.js');
var starknet = require('@scure/starknet');
var sha2_js = require('@noble/hashes/sha2.js');
var legacy_js = require('@noble/hashes/legacy.js');
var functions = require('./functions.js');
var errors = require('./errors.js');
var Precise = require('./Precise.js');
var WsClient = require('./ws/WsClient.js');
var Future = require('./ws/Future.js');
var OrderBook = require('./ws/OrderBook.js');
var totp = require('./functions/totp.js');
var index = require('../static_dependencies/ethers/index.js');
require('../static_dependencies/ethers/utils/base58.js');
require('../static_dependencies/ethers/utils/errors.js');
require('../static_dependencies/ethers/utils/fixednumber.js');
require('../static_dependencies/ethers/utils/maths.js');
require('../static_dependencies/ethers/utils/utf8.js');
require('../static_dependencies/ethers/address/address.js');
var typedData = require('../static_dependencies/ethers/hash/typed-data.js');
var zklinkSdkWeb = require('../static_dependencies/zklink/zklink-sdk-web.js');
require('@noble/curves/abstract/poseidon.js');
var selector = require('../static_dependencies/starknet/utils/selector.js');
var classHash = require('../static_dependencies/starknet/utils/hash/classHash.js');
var index$1 = require('../static_dependencies/starknet/utils/calldata/index.js');
var typedData$1 = require('../static_dependencies/starknet/utils/typedData.js');
require('../static_dependencies/dydx-v4-client/helpers.js');
var index$2 = require('../static_dependencies/dydx-v4-client/long/index.cjs.js');
var io = require('./functions/io.js');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
// ----------------------------------------------------------------------------
const { isNode, isBun, selfIsDefined, deepExtend, extend, clone, unique, indexBy, sortBy, sortBy2, safeFloat2, groupBy, aggregate, uuid, unCamelCase, precisionFromString, Throttler, capitalize, now, decimalToPrecision, safeValue, safeValue2, safeString, safeString2, seconds, milliseconds, binaryToBase16, numberToBE, base16ToBinary, iso8601, omit, isJsonEncodedObject, safeInteger, sum, omitZero, implodeParams, extractParams, json, binaryConcat, hash,
// ecdsa,
arrayConcat, encode, urlencode, hmac, numberToString, roundTimeframe, parseTimeframe, safeInteger2, safeStringLower, parse8601, yyyymmdd, safeStringUpper, safeTimestamp, binaryConcatArray, ymdhms, stringToBase64, decode, uuid22, safeIntegerProduct2, safeIntegerProduct, safeStringLower2, yymmdd, base58ToBinary, binaryToBase58, safeTimestamp2, rawencode, keysort, sort, inArray, isEmpty, filterBy, uuid16, safeFloat, base64ToBinary, safeStringUpper2, urlencodeWithArrayRepeat, microseconds, binaryToBase64, strip, toArray, safeFloatN, safeIntegerN, safeIntegerProductN, safeTimestampN, safeValueN, safeStringN, safeStringLowerN, safeStringUpperN, urlencodeNested, urlencodeBase64, parseDate, ymd, base64ToString, crc32, packb, TRUNCATE, ROUND, DECIMAL_PLACES, NO_PADDING, TICK_SIZE, SIGNIFICANT_DIGITS, sleep, readFile, writeFile, existsFile, getTempDir, filePathToFileUrlForWindows, } = functions;
// ----------------------------------------------------------------------------
//
const dynamicImport = async (moduleName) => await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(/* webpackIgnore: true */ moduleName);
//
// ----------------------------------------------------------------------------
//
let protobufMexc = undefined;
let encodeAsAny = undefined;
let AuthInfo = undefined;
let Tx = undefined;
let TxBody = undefined;
let TxRaw = undefined;
let SignDoc = undefined;
let SignMode = undefined;
// onJsonResponse regex, hoisted to module scope (shared safely: String.replace resets
// lastIndex on the global regex) - profiled neutral vs a function-local literal
// (<2%, within noise, V8 caches the compiled literal anyway), kept hoisted for tidiness
const QUOTE_JSON_NUMBERS_REGEX = /":([+.0-9eE-]+)(?=[,}])/g;
// -----------------------------------------------------------------------------
/**
* @class Exchange
*/
class BaseExchange {
// this is updated by vss.js when building
static { this.ccxtVersion = '4.5.70'; }
constructor(userConfig = {}) {
this.isSandboxModeEnabled = false;
this.certified = false;
this.pro = false;
this.countries = undefined;
this.proxyUrl = undefined;
this.proxy_url = undefined;
this.httpProxy = undefined;
this.http_proxy = undefined;
this.httpsProxy = undefined;
this.https_proxy = undefined;
this.socksProxy = undefined;
this.socks_proxy = undefined;
this.userAgent = undefined;
this.user_agent = undefined;
this.wsProxy = undefined;
this.ws_proxy = undefined;
this.wssProxy = undefined;
this.wss_proxy = undefined;
this.wsSocksProxy = undefined;
this.ws_socks_proxy = undefined;
//
this.userAgents = {
'chrome': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'chrome39': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
'chrome100': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36',
};
this.headers = {};
this.returnResponseHeaders = false;
this.origin = '*'; // CORS origin
this.MAX_VALUE = Number.MAX_VALUE;
//
this.agent = undefined; // maintained for backwards compatibility
this.httpAgent = undefined;
this.httpsAgent = undefined;
this.minFundingAddressLength = 1; // used in checkAddress
this.substituteCommonCurrencyCodes = true; // reserved
this.quoteJsonNumbers = true; // quote json numbers as precise strings when a response carries precision-risky (16+ digit or exponent) numbers
// eslint-disable-next-line no-unused-vars
this.number = Number; // or String (a pointer to a function)
this.handleContentTypeApplicationZip = false;
// whether fees should be summed by currency code
this.reduceFees = true;
this.fetchIsNative = false;
this.undiciModule = undefined;
this.zlibModule = undefined; // node:zlib, for transparent response decompression on the undici.request path
this.httpStatusTexts = {}; // node:http STATUS_CODES, undici.request carries no reason phrases
this.fetchDispatcher = undefined; // shared keep-alive undici Agent (node only)
this.validateServerSsl = true;
this.validateClientSsl = false;
this.timeout = 10000; // milliseconds
this.verbose = false;
this.balance = {};
this.liquidations = undefined;
this.orderbooks = {};
this.tickers = {};
this.fundingRates = {};
this.bidsasks = {};
this.orders = undefined;
this.transactions = {};
this.myLiquidations = undefined;
this.requiresWeb3 = false;
this.enableLastJsonResponse = false;
this.enableLastHttpResponse = true;
this.enableLastResponseHeaders = true;
this.last_http_response = undefined;
this.last_json_response = undefined;
this.last_response_headers = undefined;
this.last_request_headers = undefined;
this.last_request_body = undefined;
this.last_request_url = undefined;
this.last_request_path = undefined;
this.fetchHistoryCache = [];
this.fetchHistoryCacheSize = 0;
this.id = 'Exchange';
this.rateLimit = 2000; // milliseconds
this.throttler = undefined;
this.enableRateLimit = true;
this.rollingWindowSize = 0.0; // set to 0.0 to use leaky bucket rate limiter
this.rateLimiterAlgorithm = 'leakyBucket';
this.symbols = [];
this.ids = undefined;
this.currencies = {};
this.baseCurrencies = undefined;
this.quoteCurrencies = undefined;
this.currencies_by_id = undefined;
this.codes = undefined;
this.reloadingMarkets = undefined;
this.hostname = undefined;
this.precisionMode = undefined;
this.paddingMode = undefined;
// nested by design: exchanges declare { 'exact': {...}, 'broad': {...} }
// and pro subclasses add a 'ws' sub-tree, with error classes as values
this.exceptions = {};
this.timeframes = {};
this.version = undefined;
this.name = undefined;
this.httpProxyAgentModule = undefined;
this.httpsProxyAgentModule = undefined;
this.socksProxyAgentModule = undefined;
this.socksProxyAgentModuleChecked = false;
this.proxyDictionaries = {}; // proxyUrl -> undici dispatcher (native fetch path) or node-style agent (legacy fetchImplementation / ws paths), distinguished by the 'dispatch' method
this.proxyDictionariesMaxSize = 8; // FIFO cap - an undici dispatcher entry holds ~23KB of eagerly-allocated pool state, so rotating-proxy setups would otherwise grow the cache without bound; evicted undici dispatchers are closed gracefully, evicted legacy agents are just dropped (they may still back live ws connections)
this.alias = false;
// WS/PRO options
this.clients = {};
this.newUpdates = true;
this.streaming = {};
// INTERNAL METHODS
this.sleep = sleep;
this.deepExtend = deepExtend;
this.deepExtendSafe = deepExtend;
this.isNode = isNode;
this.extend = extend;
this.clone = clone;
this.unique = unique;
this.indexBy = indexBy;
this.indexBySafe = indexBy;
this.roundTimeframe = roundTimeframe;
this.sortBy = sortBy;
this.sortBy2 = sortBy2;
this.groupBy = groupBy;
this.aggregate = aggregate;
this.uuid = uuid;
this.unCamelCase = unCamelCase;
this.precisionFromString = precisionFromString;
this.capitalize = capitalize;
this.now = now;
this.decimalToPrecision = decimalToPrecision;
this.safeValue = safeValue;
this.safeValue2 = safeValue2;
this.safeString = safeString;
this.safeString2 = safeString2;
this.safeFloat = safeFloat;
this.safeFloat2 = safeFloat2;
this.seconds = seconds;
this.milliseconds = milliseconds;
this.binaryToBase16 = binaryToBase16;
this.numberToBE = numberToBE;
this.base16ToBinary = base16ToBinary;
this.iso8601 = iso8601;
this.omit = omit;
this.isJsonEncodedObject = isJsonEncodedObject;
this.safeInteger = safeInteger;
this.sum = sum;
this.omitZero = omitZero;
this.implodeParams = implodeParams;
this.extractParams = extractParams;
this.json = json;
this.binaryConcat = binaryConcat;
this.hash = hash;
this.arrayConcat = arrayConcat;
this.encode = encode;
this.urlencode = urlencode;
this.hmac = hmac;
this.numberToString = numberToString;
this.parseTimeframe = parseTimeframe;
this.safeInteger2 = safeInteger2;
this.safeStringLower = safeStringLower;
this.parse8601 = parse8601;
this.yyyymmdd = yyyymmdd;
this.safeStringUpper = safeStringUpper;
this.safeTimestamp = safeTimestamp;
this.binaryConcatArray = binaryConcatArray;
this.ymdhms = ymdhms;
this.yymmdd = yymmdd;
this.stringToBase64 = stringToBase64;
this.decode = decode;
this.uuid22 = uuid22;
this.safeIntegerProduct2 = safeIntegerProduct2;
this.safeIntegerProduct = safeIntegerProduct;
this.binaryToBase58 = binaryToBase58;
this.base58ToBinary = base58ToBinary;
this.base64ToBinary = base64ToBinary;
this.safeTimestamp2 = safeTimestamp2;
this.rawencode = rawencode;
this.keysort = keysort;
this.sort = sort;
this.inArray = inArray;
this.safeStringLower2 = safeStringLower2;
this.safeStringUpper2 = safeStringUpper2;
this.isEmpty = isEmpty;
this.filterBy = filterBy;
this.uuid16 = uuid16;
this.urlencodeWithArrayRepeat = urlencodeWithArrayRepeat;
this.microseconds = microseconds;
this.binaryToBase64 = binaryToBase64;
this.strip = strip;
this.toArray = toArray;
this.safeFloatN = safeFloatN;
this.safeIntegerN = safeIntegerN;
this.safeIntegerProductN = safeIntegerProductN;
this.safeTimestampN = safeTimestampN;
this.safeValueN = safeValueN;
this.safeStringN = safeStringN;
this.safeStringLowerN = safeStringLowerN;
this.safeStringUpperN = safeStringUpperN;
this.urlencodeNested = urlencodeNested;
this.parseDate = parseDate;
this.ymd = ymd;
this.base64ToString = base64ToString;
this.crc32 = crc32;
this.packb = packb;
this.urlencodeBase64 = urlencodeBase64;
// io
this.readFile = readFile;
this.writeFile = writeFile;
this.existsFile = existsFile;
this.getTempDir = getTempDir;
Object.assign(this, functions);
//
// if (isNode) {
// this.nodeVersion = process.version.match (/\d+\.\d+\.\d+/)[0]
// this.userAgent = {
// 'User-Agent': 'ccxt/' + Exchange.ccxtVersion +
// ' (+https://github.com/ccxt/ccxt)' +
// ' Node.js/' + this.nodeVersion + ' (JavaScript)'
// }
// }
//
this.options = this.getDefaultOptions(); // exchange-specific options if any
// fetch implementation options (JS only)
// http properties
this.headers = {};
this.origin = '*'; // CORS origin
// underlying properties
this.minFundingAddressLength = 1; // used in checkAddress
this.substituteCommonCurrencyCodes = true; // reserved
this.quoteJsonNumbers = true; // quote json numbers as precise strings when a response carries precision-risky numbers
this.number = Number; // or String (a pointer to a function)
this.handleContentTypeApplicationZip = false;
// whether fees should be summed by currency code
this.reduceFees = true;
// do not delete this line, it is needed for users to be able to define their own fetchImplementation
this.fetchImplementation = undefined;
this.validateServerSsl = true;
this.validateClientSsl = false;
// default property values
this.timeout = 10000; // milliseconds
this.verbose = false;
// placeholders for cached data
this.balance = {};
this.bidsasks = {};
this.orderbooks = {};
this.tickers = {};
this.liquidations = undefined;
this.orders = undefined;
this.trades = {};
this.transactions = {};
this.ohlcvs = {};
this.myLiquidations = undefined;
this.myTrades = undefined;
this.positions = undefined;
// web3 and cryptography flags
this.requiresWeb3 = false;
// response handling flags and properties
this.lastRestRequestTimestamp = 0;
this.enableLastJsonResponse = false;
this.enableLastHttpResponse = true;
this.enableLastResponseHeaders = true;
this.last_json_response = undefined;
this.last_request_body = undefined;
// camelCase and snake_notation support
const unCamelCaseProperties = (obj = this) => {
if (obj !== null) {
const ownPropertyNames = Object.getOwnPropertyNames(obj);
for (let i = 0; i < ownPropertyNames.length; i++) {
const k = ownPropertyNames[i];
this[unCamelCase(k)] = this[k];
}
unCamelCaseProperties(Object.getPrototypeOf(obj));
}
};
unCamelCaseProperties();
// merge constructor overrides to this instance
const configEntries = Object.entries(this.describe()).concat(Object.entries(userConfig));
for (let i = 0; i < configEntries.length; i++) {
const [property, value] = configEntries[i];
if (value && Object.getPrototypeOf(value) === Object.prototype) {
this[property] = this.deepExtend(this[property], value);
}
else {
this[property] = value;
}
}
// generate old metainfo interface
const hasKeys = Object.keys(this.has);
for (let i = 0; i < hasKeys.length; i++) {
const k = hasKeys[i];
this['has' + this.capitalize(k)] = !!this.has[k]; // converts 'emulated' to true
}
// generate implicit api
if (this.api) {
this.defineRestApi(this.api, 'request');
}
this.newUpdates = (this.options.newUpdates !== undefined) ? this.options.newUpdates : true;
this.afterConstruct();
if (this.safeBool(userConfig, 'sandbox') || this.safeBool(userConfig, 'testnet')) {
this.setSandboxMode(true);
}
// exchange specific libs
this.loadExchangeSpecificFiles();
}
async loadExchangeSpecificFiles() {
if (this.id === 'mexc') {
try {
protobufMexc = await Promise.resolve().then(function () { return require('../protobuf/mexc/compiled.cjs.js'); });
}
catch (e) {
// TODO: handle error
}
}
}
uuid5(namespace, name) {
const nsBytes = namespace
.replace(/-/g, '')
.match(/.{1,2}/g)
.map((byte) => parseInt(byte, 16));
const nameBytes = new TextEncoder().encode(name);
const data = new Uint8Array([...nsBytes, ...nameBytes]);
const nsHash = legacy_js.sha1(data);
nsHash[6] = (nsHash[6] & 0x0f) | 0x50;
nsHash[8] = (nsHash[8] & 0x3f) | 0x80;
const hex = [...nsHash.slice(0, 16)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return [
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20, 32),
].join('-');
}
encodeURIComponent(...args) {
// @ts-expect-error
return encodeURIComponent(...args);
}
throttle(cost = undefined) {
return this.throttler.throttle(cost);
}
initThrottler() {
this.throttler = new Throttler(this.tokenBucket);
}
defineRestApiEndpoint(methodName, uppercaseMethod, lowercaseMethod, camelcaseMethod, path, paths, config = {}) {
const splitPath = path.split(/[^a-zA-Z0-9]/);
const camelcaseSuffix = splitPath.map(this.capitalize).join('');
const underscoreSuffix = splitPath.map((x) => x.trim().toLowerCase()).filter((x) => x.length > 0).join('_');
const camelcasePrefix = [paths[0]].concat(paths.slice(1).map(this.capitalize)).join('');
const underscorePrefix = [paths[0]].concat(paths.slice(1).map((x) => x.trim()).filter((x) => x.length > 0)).join('_');
const camelcase = camelcasePrefix + camelcaseMethod + this.capitalize(camelcaseSuffix);
const underscore = underscorePrefix + '_' + lowercaseMethod + '_' + underscoreSuffix;
const typeArgument = (paths.length > 1) ? paths : paths[0];
// handle call costs here
const partial = async (params = {}, context = {}) => this[methodName](path, typeArgument, uppercaseMethod, params, undefined, undefined, config, context);
// const partial = async (params) => this[methodName] (path, typeArgument, uppercaseMethod, params || {})
this[camelcase] = partial;
this[underscore] = partial;
}
defineRestApi(api, methodName, paths = []) {
const keys = Object.keys(api);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = api[key];
const uppercaseMethod = key.toUpperCase();
const lowercaseMethod = key.toLowerCase();
const camelcaseMethod = this.capitalize(lowercaseMethod);
if (Array.isArray(value)) {
for (let k = 0; k < value.length; k++) {
const path = value[k].trim();
this.defineRestApiEndpoint(methodName, uppercaseMethod, lowercaseMethod, camelcaseMethod, path, paths);
}
// the options HTTP method conflicts with the 'options' API url path
// } else if (key.match (/^(?:get|post|put|delete|options|head|patch)$/i)) {
}
else if (key.match(/^(?:get|post|put|delete|head|patch)$/i)) {
const endpoints = Object.keys(value);
for (let j = 0; j < endpoints.length; j++) {
const endpoint = endpoints[j];
const path = endpoint.trim();
const config = value[endpoint];
if (typeof config === 'object') {
this.defineRestApiEndpoint(methodName, uppercaseMethod, lowercaseMethod, camelcaseMethod, path, paths, config);
}
else if (typeof config === 'number') {
this.defineRestApiEndpoint(methodName, uppercaseMethod, lowercaseMethod, camelcaseMethod, path, paths, { 'cost': config });
}
else {
throw new errors.NotSupported(this.id + ' defineRestApi() API format is not supported, API leafs must strings, objects or numbers');
}
}
}
else {
this.defineRestApi(value, methodName, paths.concat([key]));
}
}
}
log(...args) {
console.log(...args);
}
async loadProxyModules() {
// when loading markets, multiple parallel calls are made, so need one promise
if (this.proxiesModulesLoading === undefined) {
this.proxiesModulesLoading = (async () => {
// the vendored proxy agents were removed together with the node-fetch transport -
// the native fetch paths handle proxies through undici (see getFetchProxyDispatcher),
// so the node-style agent modules below back only the legacy paths (user-supplied
// fetchImplementation and ws connections) and are optional user-installed packages
try {
// @ts-ignore
this.httpProxyAgentModule = await dynamicImport('http-proxy-agent');
// @ts-ignore
this.httpsProxyAgentModule = await dynamicImport('https-proxy-agent');
}
catch (e) {
// when they are actually needed (in setProxyAgents) 'NotSupported' exception with install instructions is already raised
}
if (this.socksProxyAgentModuleChecked === false) {
try {
// @ts-ignore
this.socksProxyAgentModule = await dynamicImport('socks-proxy-agent');
}
catch (e) {
// optional module, setProxyAgents raises NotSupported with install instructions when it is actually needed
}
this.socksProxyAgentModuleChecked = true;
}
})();
}
return await this.proxiesModulesLoading;
}
setProxyAgents(httpProxy, httpsProxy, socksProxy) {
let chosenAgent = undefined;
// in browser-side, proxy modules are not supported in 'fetch/ws' methods
if (!isNode && (httpProxy || httpsProxy || socksProxy)) {
throw new errors.NotSupported(this.id + ' - proxies in browser-side projects are not supported. You have several choices: [A] Use `exchange.proxyUrl` property to redirect requests through local/remote cors-proxy server (find sample file named "sample-local-proxy-server-with-cors" in https://github.com/ccxt/ccxt/tree/master/examples/ folder, which can be used for REST requests only) [B] override `exchange.fetch` && `exchange.watch` methods to send requests through your custom proxy');
}
if (httpProxy) {
if (this.httpProxyAgentModule === undefined) {
throw new errors.NotSupported(this.id + ' - to use httpProxy with ccxt, at first you need install module "npm i http-proxy-agent" and then initialize proxies with `await instance.loadProxyModules()` method');
}
const cachedHttpAgent = this.proxyDictionaries[httpProxy];
if ((cachedHttpAgent === undefined) || (typeof cachedHttpAgent.dispatch === 'function')) {
// absent, or the url is occupied by an undici dispatcher from the native fetch path - (re)create the node-style agent
this.cacheProxyDictionary(httpProxy, new this.httpProxyAgentModule.HttpProxyAgent(httpProxy));
}
chosenAgent = this.proxyDictionaries[httpProxy];
}
else if (httpsProxy) {
if (this.httpsProxyAgentModule === undefined) {
throw new errors.NotSupported(this.id + ' - to use httpsProxy with ccxt, at first you need install module "npm i https-proxy-agent" and then initialize proxies with `await instance.loadProxyModules()` method');
}
const cachedHttpsAgent = this.proxyDictionaries[httpsProxy];
if ((cachedHttpsAgent === undefined) || (typeof cachedHttpsAgent.dispatch === 'function')) {
this.cacheProxyDictionary(httpsProxy, new this.httpsProxyAgentModule.HttpsProxyAgent(httpsProxy));
}
chosenAgent = this.proxyDictionaries[httpsProxy];
if (chosenAgent === undefined) {
throw new errors.ExchangeError(this.id + ' setProxyAgents() missing chosenAgent');
}
chosenAgent.keepAlive = true;
}
else if (socksProxy) {
if (this.socksProxyAgentModule === undefined) {
throw new errors.NotSupported(this.id + ' - to use SOCKS proxy with ccxt, at first you need install module "npm i socks-proxy-agent" and then initialize proxies with `await instance.loadProxyModules()` method');
}
const cachedSocksAgent = this.proxyDictionaries[socksProxy];
if ((cachedSocksAgent === undefined) || (typeof cachedSocksAgent.dispatch === 'function')) {
this.cacheProxyDictionary(socksProxy, new this.socksProxyAgentModule.SocksProxyAgent(socksProxy));
}
chosenAgent = this.proxyDictionaries[socksProxy];
}
return chosenAgent;
}
async loadHttpProxyAgent() {
// for `http://` protocol proxy-urls, we need to load `http` module only on first call
if (!this.httpAgent) {
const httpModule = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(/* webpackIgnore: true */ 'node:http')); });
this.httpAgent = new httpModule.Agent();
}
return this.httpAgent;
}
getHttpAgentIfNeeded(url) {
if (isNode) {
// only for non-ssl proxy
if (url.substring(0, 5) === 'ws://') {
if (this.httpAgent === undefined) {
throw new errors.NotSupported(this.id + ' to use proxy with non-ssl ws:// urls, at first run `await exchange.loadHttpProxyAgent()` method');
}
return this.httpAgent;
}
}
return undefined;
}
addFetchCache(data) {
if (this.fetchHistoryCacheSize <= 0) {
return;
}
if (this.fetchHistoryCache.length >= this.fetchHistoryCacheSize) {
this.fetchHistoryCache.shift();
}
this.fetchHistoryCache.push(data);
}
getFetchCache() {
return this.fetchHistoryCache;
}
isBinaryMessage(msg) {
return msg instanceof Uint8Array || msg instanceof ArrayBuffer;
}
stringToBinary(content) {
// same as: this.base64ToBinary (this.stringToBase64 (str));
return this.encode(content);
}
binaryToString(binary) {
return this.decode(binary);
}
decodeProtoMsg(data) {
if (!protobufMexc) {
throw new errors.NotSupported(this.id + ' requires protobuf to decode messages, please install it with `npm install protobufjs`');
}
if (data instanceof ArrayBuffer) {
// browser case
data = new Uint8Array(data);
}
if (data instanceof Uint8Array) {
const decoded = protobufMexc.default.PushDataV3ApiWrapper.decode(data);
const dict = decoded.toJSON();
// {
// "channel":"spot@public.kline.v3.api.pb@BTCUSDT@Min1",
// "symbol":"BTCUSDT",
// "symbolId":"2fb942154ef44a4ab2ef98c8afb6a4a7",
// "createTime":"1754737941062",
// "publicSpotKline":{
// "interval":"Min1",
// "windowStart":"1754737920",
// "openingPrice":"117317.31",
// "closingPrice":"117325.26",
// "highestPrice":"117341",
// "lowestPrice":"117317.3",
// "volume":"3.12599854",
// "amount":"366804.43",
// "windowEnd":"1754737980"
// }
// }
return dict;
}
return data;
}
/**
* @ignore
* @method
* @name Exchange#loadFetchImplementation
* @description resolves the fetch implementation once per instance - the platform-native fetch is used everywhere (undici in node, native fetch in bun / browsers / deno), a user-supplied this.fetchImplementation always takes precedence
* @returns {Promise<any>} a promise that resolves when the fetch client is ready
*/
async loadFetchImplementation() {
// one shared promise, so parallel first-requests initialize the client only once
if (this.fetchImplementationLoading === undefined) {
this.fetchImplementationLoading = (async () => {
if (this.fetchImplementation !== undefined) {
// user-supplied custom implementation - ensure the error classes have sane defaults
if (this.AbortError === undefined) {
this.AbortError = DOMException;
}
if (this.FetchError === undefined) {
this.FetchError = TypeError;
}
return;
}
this.AbortError = DOMException;
this.FetchError = TypeError;
// in the browser (and webworkers / deno / bun) default to the platform's
// built-in fetch - the undici import below is never even attempted there
if (!isNode || isBun) {
if (selfIsDefined()) {
// fetchImplementation cannot be called on this. in browsers:
// TypeError Failed to execute 'fetch' on 'Window': Illegal invocation
// eslint-disable-next-line
this.fetchImplementation = self.fetch;
}
else if (typeof fetch === 'function') {
this.fetchImplementation = fetch;
}
else {
throw new errors.NotSupported(this.id + ' the built-in "fetch" function is not available in this environment, please use a modern browser, bun, or node.js 18+');
}
this.fetchIsNative = true;
return;
}
// node: prefer the undici module for tunable pooling and proxy support
try {
// undici is the engine behind node's built-in fetch - importing it directly gives us
// tunable connection pooling (keep-alive), ProxyAgent support, and the low-level
// undici.request api used in undiciRequest (~2x faster than undici.fetch, profiled
// in bench-request.mjs: no WHATWG Response/Headers/web-streams machinery)
//
// note: undici is pinned to 7.27.x in package.json - starting with 7.28/8.x undici
// unconditionally defers every write on an idle kept-alive socket behind a
// setTimeout(0) tick ("idle socket validation", the mitigation for GHSA-35p6-xmwp-9g52),
// which adds ~1.3ms to every sequential request; 7.27.x is the last line without that
// penalty (profiled against a localhost server: 0.22ms/req on 7.27.2 vs 1.3ms/req on
// 8.5.0) - see https://github.com/nodejs/undici/issues/5493 for the upstream fix
const undiciModule = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(/* webpackIgnore: true */ 'undici')); });
this.undiciModule = undiciModule;
this.fetchImplementation = undiciModule.fetch;
// redirects are never followed - exchange rest apis do not redirect legitimately,
// so a redirect response raises NetworkError instead (see undiciRequest, and the
// 'redirect': 'error' request param on the whatwg-fetch paths)
this.fetchDispatcher = new undiciModule.Agent(this.getDispatcherOptions(true));
// node:zlib for transparent gzip/br/deflate response decompression (see undiciBody),
// node:http for the reason phrases that undici.request responses do not carry
this.zlibModule = await Promise.resolve().then(function () { return require(/* webpackIgnore: true */ 'node:zlib'); });
const httpModule = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(/* webpackIgnore: true */ 'node:http')); });
this.httpStatusTexts = httpModule.STATUS_CODES;
this.fetchIsNative = true;
return;
}
catch (e) {
// some environments cannot resolve the undici module (custom bundlers, https://github.com/ccxt/ccxt/pull/20687)
// fall through to node's built-in global fetch below (same undici engine, default pooling)
}
if (typeof fetch === 'function') {
this.fetchImplementation = fetch;
this.fetchIsNative = true;
}
else {
throw new errors.NotSupported(this.id + ' the built-in "fetch" function is not available in this environment, please use node.js 18+, bun, or a modern browser');
}
})();
}
return await this.fetchImplementationLoading;
}
/**
* @ignore
* @method
* @name Exchange#getDispatcherOptions
* @description builds keep-alive-tuned undici dispatcher options - every in-flight request gets its own socket (no pipelining, no h2 multiplexing), idle sockets are kept alive for reuse because exchanges are polled on the same origins repeatedly - dual-stack is explicit: autoSelectFamily enables the happy eyeballs (rfc 8305) address-family racing so ipv6 and ipv4 are both attempted (off by default on node 18), without forcing either family
* @param {boolean} [isPlainAgent] true for undici.Agent options ('connect' tls shape), false for undici.ProxyAgent options ('requestTls' shape)
* @returns {object} undici dispatcher options
*/
getDispatcherOptions(isPlainAgent = false) {
const options = {
'keepAliveTimeout': 60 * 1000, // hold idle sockets for 60s (server keep-alive hints still apply)
'keepAliveMaxTimeout': 10 * 60 * 1000, // cap server-suggested keep-alive at 10 minutes
'connections': 256, // per-origin socket cap, prevents fd exhaustion under bursts
'pipelining': 1, // one in-flight request per socket - concurrent requests never share a socket, each opens (or reuses an idle) one
'allowH2': false, // force HTTP/1.1 - h2 would multiplex concurrent requests over one shared socket
'autoSelectFamily': true, // happy eyeballs (rfc 8305) - race ipv6 against ipv4 instead of relying on dns answer order, dual-stack instead of accidental ipv4-only
'autoSelectFamilyAttemptTimeout': 10, // ms before starting the parallel attempt to the next address family - 10ms is node's floor (lower values are clamped up, 0 is rejected), so the next family is raced almost immediately (near-parallel) instead of after a long serial stall
};
if (!this.shouldValidateServerSsl()) {
const tlsOptions = { 'rejectUnauthorized': false };
if (isPlainAgent) {
options['connect'] = tlsOptions;
}
else {
options['requestTls'] = tlsOptions;
}
}
return options;
}
/**
* @ignore
* @method
* @name Exchange#shouldValidateServerSsl
* @description whether server certificates should be validated, honoring both this.validateServerSsl and legacy agents constructed with rejectUnauthorized false
* @returns {boolean} true when server ssl certificates must be validated
*/
shouldValidateServerSsl() {
if (!this.validateServerSsl) {
return false;
}
// backwards compatibility: users disabled tls validation by passing an agent object
const legacyAgents = [this.agent, this.httpsAgent];
for (let i = 0; i < legacyAgents.length; i++) {
const legacyAgent = legacyAgents[i];
if (legacyAgent && legacyAgent.options && (legacyAgent.options.rejectUnauthorized === false)) {
return false;
}
}
return true;
}
/**
* @ignore
* @method
* @name Exchange#extractProxyFromAgent
* @description backwards compatibility helper - http-proxy-agent, https-proxy-agent and socks-proxy-agent instances all carry their target on a `proxy` property (URL object or string), extract it so it can be passed to the native fetch
* @param {object} [agent] a legacy proxy-carrying agent object
* @returns {string|undefined} the proxy url, or undefined when the agent does not carry one
*/
extractProxyFromAgent(agent) {
if (agent !== undefined && agent !== null) {
const proxy = agent.proxy;
if (proxy !== undefined && proxy !== null) {
if (typeof proxy === 'string') {
return proxy;
}
if (proxy.href !== undefined) {
return proxy.href; // URL instance
}
}
}
return undefined;
}
/**
* @ignore
* @method
* @name Exchange#cacheProxyDictionary
* @description stores a per-proxy-url transport handle (undici dispatcher or legacy node-style agent) in this.proxyDictionaries, evicting the oldest entry beyond proxyDictionariesMaxSize so rotating-proxy setups cannot grow the cache without bound - evicted or replaced undici dispatchers are closed gracefully (in-flight requests finish first), legacy agents are just dropped because live ws connections may still hold them
* @param {string} proxyUrl the proxy url the handle serves
* @param {any} value an undici dispatcher or a node-style agent
* @returns {any} the cached value
*/
cacheProxyDictionary(proxyUrl, value) {
const existing = this.proxyDictionaries[proxyUrl];
if (existing !== undefined) {
// same url switching between the dispatcher and agent families
this.releaseProxyDictionaryEntry(existing);
}
else {
const cachedProxyUrls = Object.keys(this.proxyDictionaries);
if (cachedProxyUrls.length >= this.proxyDictionariesMaxSize) {
const oldestProxyUrl = cachedProxyUrls[0]; // js objects preserve string-key insertion order
const oldestEntry = this.proxyDictionaries[oldestProxyUrl];
delete this.proxyDictionaries[oldestProxyUrl];
this.releaseProxyDictionaryEntry(oldestEntry);
}
}
this.proxyDictionaries[proxyUrl] = value;
return value;
}
/**
* @ignore
* @method
* @name Exchange#releaseProxyDictionaryEntry
* @description closes an undici dispatcher that left this.proxyDictionaries - close () drains in-flight requests before releasing sockets, and any close failure is irrelevant because the dispatcher is already unreferenced; legacy node-style agents are left untouched (never destroyed, matching the long-standing behavior, because live ws connections may still use them)
* @param {any} entry the evicted or replaced proxyDictionaries value
*/
releaseProxyDictionaryEntry(entry) {
if ((entry !== undefined) && (entry !== null) && (typeof entry.dispatch === 'function') && (typeof entry.close === 'function')) {
entry.close().catch(() => { });
}
}
/**
* @ignore
* @method
* @name Exchange#getFetchProxyDispatcher
* @description returns a cached undici ProxyAgent dispatcher for the given proxy url - undici handles http, https, socks5 and socks schemes natively; dispatchers share this.proxyDictionaries with the legacy node-style agents (entries are distinguished by the 'dispatch' method) and the cache is FIFO-capped at proxyDictionariesMaxSize entries so rotating-proxy setups cannot grow it without bound
* @param {string} proxyUrl the proxy url, e.g. http://user:pass@host:port or socks5://host:port
* @returns {any} an undici dispatcher
*/
getFetchProxyDispatcher(proxyUrl) {
const cached = this.proxyDictionaries[proxyUrl];
if ((cached !== undefined) && (typeof cached.dispatch === 'function')) {
return cached;
}
// absent, or the url is occupied by a legacy node-style agent - (re)create the dispatcher
const lowercaseProxyUrl = proxyUrl.toLowerCase();
if ((lowercaseProxyUrl.indexOf('socks4:') === 0) || (lowercaseProxyUrl.indexOf('socks4a:') === 0)) {
throw new errors.NotSupported(this.id + ' - socks4 proxies are not supported by the built-in fetch, please use a socks5:// proxy');
}
const options = this.getDispatcherOptions(false);
options['uri'] = proxyUrl;
return this.cacheProxyDictionary(proxyUrl, new this.undiciModule.ProxyAgent(options));
}
/**
* @ignore
* @method
* @name Exchange#undiciRequest
* @description dispatches through the low-level undici.request api and adapts the result to the minimal fetch-Response surface that handleRestResponse consumes - profiled ~2x faster end-to-end than undici.fetch (node streams instead of the WHATWG Response/Headers/web-streams machinery)
* @param {string} url the request url
* @param {object} params fetch-style request params ('method', 'headers', 'body', 'signal', 'dispatcher')
* @returns {Promise<object>} an object with 'status', 'statusText', 'headers' (plain object), 'text' and 'arrayBuffer' methods
*/
async undiciRequest(url, params) {
const headers = params['headers'];
if (!('Accept-Encoding' in headers) && !('accept-encoding' in headers)) {
headers['Accept-Encoding'] = 'gzip, deflate, br'; // fetch/node-fetch parity, inflated in undiciBody
}
const res = await this.undiciModule.request(url, { 'method': params['method'], 'headers': headers, 'body': params['body'], 'signal': params['signal'], 'dispatcher': params['dispatcher'] });
// undici.request follows no redirects by default and none are composed onto the
// dispatchers - exchange rest apis do not redirect legitimately, so surface a
// redirect response as a NetworkError instead of following it or handing its
// (usually empty) body to handleRestResponse
const statusCode = res.statusCode;
const location = res.headers['location'];
if ((statusCode >= 300) && (statusCode < 400) && (location !== undefined)) {
try {
await res.body.dump(); // drain the body so the socket returns to the keep-alive pool
}
catch (e) {
// a drain failure only costs the connection, the throw below is what matters
}
throw new errors.NetworkError(this.id + ' ' + params['method'] + ' ' + url + ' redirect to ' + location + ' was not followed (HTTP ' + statusCode + ', following redirects is disabled)');
}
return {
'status': res.statusCode,
'statusText': this.httpStatusTexts[res.statusCode] || '',
'headers': res.headers,
'text': () => this.undiciBody(res, false),
'arrayBuffer': () => this.undiciBody(res, true),
};
}
/**
* @ignore
* @method
* @name Exchange#undiciBody
* @description reads an undici.request response body, transparently decompressing gzip/br/deflate content-encodings via node:zlib (undici.request performs no decompression, unlike fetch)
* @param {object} res an undici.request response
* @param {boolean} [binary] true to return a Buffer, false to return a utf8 string
* @returns {Promise<any>} the response body
*/
async undiciBody(res, binary = false) {
let contentEncoding = res.headers['content-encoding'];
if ((res.statusCode === 204) || (res.statusCode === 304)) {
contentEncoding = undefined; // bodyless statuses, nothing to inflate
}
let decompressor = undefined;
if ((contentEncoding === 'gzip') || (contentEncoding === 'x-gzip')) {
decompressor = this.zlibModule.createGunzip();
}
else if (contentEncoding === 'br') {
decompressor = this.zlibModule.createBrotliDecompress();
}
else if (contentEncoding === 'deflate') {
decompressor = this.zlibModule.createInflate();
}
// ponytail: chained content-encodings ('gzip, br') are not handled - no exchange sends them,
// and neither did the previous node-fetch implementation
if (decompressor === undefined) {
return binary ? await res.body.arrayBuffer() : await res.body.text();
}
const chunks = [];
res.body.pipe(decompressor);
for await (const chunk of decompressor) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
return binary ? buffer : buffer.toString('utf8');
}
/**
* @ignore
* @method
* @name Exchange#setFetchProxyOptions
* @description attaches per-platform proxy transport options to the native fetch request params - undici dispatcher on node, the built-in `proxy` option on bun; the shared keep-alive dispatcher is attached when no proxy applies
* @param {object} params the fetch RequestInit params, mutated in place
* @param {string} [httpProxy] unified httpProxy setting
* @param {string} [httpsProxy] unified httpsProxy setting
* @param {string} [socksProxy] unified socksProxy setting
*/
setFetchProxyOptions(params, httpProxy, httpsProxy, socksProxy) {
// unified proxy settings take precedence over legacy proxy-carrying agent objects
let selectedProxy = httpProxy || httpsProxy || socksProxy;
if (!selectedProxy) {
selectedProxy = this.extractProxyFromAgent(this.agent) || this.extractProxyFromAgent(this.httpsAgent) || this.extractProxyFromAgent(this.httpAgent);
}
if (!selectedProxy) {
if (this.fetchDispatcher !== undefined) {
params['dispatcher'] = this.fetchDispatcher;
}
return;
}
if (!isNode) {
throw new errors.NotSupported(this.id + ' - proxies in browser-side projects are not supported. You have several choices: [A] Use `exchange.proxyUrl` property to redirect requests through local/remote cors-proxy server (find sample file named "sample-local-proxy-server-with-cors" in https://github.com/ccxt/ccxt/tree/master/examples/ folder, which can be used for REST requests only) [B] override `exchange.fetch` && `exchange.watch` methods to send requests through your custom proxy');
}
if (isBun) {
// bun's native fetch has first-class proxy support