UNPKG

ccxt

Version:

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

949 lines (944 loc) • 450 kB
'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