ccxt
Version:
1,041 lines (1,038 loc) • 348 kB
JavaScript
// ----------------------------------------------------------------------------
// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
// EDIT THE CORRESPONDENT .ts FILE INSTEAD
// ----------------------------------------------------------------------------
/* eslint-disable */
import * as functions from './functions.js';
const { isNode, selfIsDefined, deepExtend, extend, clone, flatten, 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, merge, binaryConcat, hash, ecdsa, arrayConcat, encode, urlencode, hmac, numberToString, roundTimeframe, parseTimeframe, safeInteger2, safeStringLower, parse8601, yyyymmdd, safeStringUpper, safeTimestamp, binaryConcatArray, uuidv1, numberToLE, ymdhms, stringToBase64, decode, uuid22, safeIntegerProduct2, safeIntegerProduct, safeStringLower2, yymmdd, base58ToBinary, binaryToBase58, safeTimestamp2, rawencode, keysort, sort, inArray, isEmpty, ordered, 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 } = functions;
import { keys as keysFunc, values as valuesFunc, vwap as vwapFunc } from './functions.js';
// import exceptions from "./errors.js"
import { // eslint-disable-line object-curly-newline
ExchangeError, BadSymbol, NullResponse, InvalidAddress, InvalidOrder, NotSupported, OperationFailed, BadResponse, AuthenticationError, DDoSProtection, RequestTimeout, NetworkError, InvalidProxySettings, ExchangeNotAvailable, ArgumentsRequired, RateLimitExceeded, BadRequest, UnsubscribeError } from "./errors.js";
import { Precise } from './Precise.js';
//-----------------------------------------------------------------------------
import WsClient from './ws/WsClient.js';
import { Future } from './ws/Future.js';
import { OrderBook as WsOrderBook, IndexedOrderBook, CountedOrderBook } from './ws/OrderBook.js';
// ----------------------------------------------------------------------------
//
import { axolotl } from './functions/crypto.js';
import totp from './functions/totp.js';
import ethers from '../static_dependencies/ethers/index.js';
import { TypedDataEncoder } from '../static_dependencies/ethers/hash/index.js';
import { SecureRandom } from "../static_dependencies/jsencrypt/lib/jsbn/rng.js";
import { getStarkKey, ethSigToPrivate, sign as starknetCurveSign } from '../static_dependencies/scure-starknet/index.js';
import init, * as zklink from '../static_dependencies/zklink/zklink-sdk-web.js';
import * as Starknet from '../static_dependencies/starknet/index.js';
import { sha256 } from '../static_dependencies/noble-hashes/sha256.js';
// ----------------------------------------------------------------------------
/**
* @class Exchange
*/
export default class Exchange {
constructor(userConfig = {}) {
this.isSandboxModeEnabled = false;
this.throttleProp = undefined;
this.sleep = sleep;
this.api = undefined;
this.certified = false;
this.pro = false;
this.countries = undefined;
this.userAgent = undefined;
this.user_agent = 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.origin = '*'; // CORS origin
this.MAX_VALUE = Number.MAX_VALUE;
//
this.agent = undefined; // maintained for backwards compatibility
this.nodeHttpModuleLoaded = false;
this.httpAgent = undefined;
this.httpsAgent = undefined;
this.minFundingAddressLength = 1; // used in checkAddress
this.substituteCommonCurrencyCodes = true; // reserved
this.quoteJsonNumbers = true; // treat numbers in json as quoted precise strings
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.validateServerSsl = true;
this.validateClientSsl = false;
this.timeout = 10000; // milliseconds
this.verbose = false;
this.twofa = undefined; // two-factor authentication (2-FA)
this.balance = {};
this.liquidations = {};
this.orderbooks = {};
this.tickers = {};
this.fundingRates = {};
this.bidsasks = {};
this.orders = undefined;
this.triggerOrders = undefined;
this.transactions = {};
this.myLiquidations = {};
this.requiresWeb3 = false;
this.requiresEddsa = false;
this.precision = undefined;
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.id = 'Exchange';
this.markets = undefined;
this.features = undefined;
this.status = undefined;
this.rateLimit = undefined; // milliseconds
this.tokenBucket = undefined;
this.throttler = undefined;
this.enableRateLimit = undefined;
this.httpExceptions = undefined;
this.limits = undefined;
this.markets_by_id = undefined;
this.symbols = undefined;
this.ids = undefined;
this.currencies = {};
this.baseCurrencies = undefined;
this.quoteCurrencies = undefined;
this.currencies_by_id = undefined;
this.codes = undefined;
this.reloadingMarkets = undefined;
this.marketsLoading = undefined;
this.accounts = undefined;
this.accountsById = undefined;
this.commonCurrencies = undefined;
this.hostname = undefined;
this.precisionMode = undefined;
this.paddingMode = undefined;
this.exceptions = {};
this.timeframes = {};
this.version = undefined;
this.marketsByAltname = undefined;
this.name = undefined;
this.targetAccount = undefined;
this.stablePairs = {};
// WS/PRO options
this.clients = {};
this.newUpdates = true;
this.streaming = {};
this.alias = false;
this.deepExtend = deepExtend;
this.deepExtendSafe = deepExtend;
this.isNode = isNode;
this.keys = keysFunc;
this.values = valuesFunc;
this.extend = extend;
this.clone = clone;
this.flatten = flatten;
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.vwap = vwapFunc;
this.merge = merge;
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.uuidv1 = uuidv1;
this.numberToLE = numberToLE;
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.ordered = ordered;
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;
this.httpProxyAgentModule = undefined;
this.httpsProxyAgentModule = undefined;
this.socksProxyAgentModule = undefined;
this.socksProxyAgentModuleChecked = false;
this.proxyDictionaries = {};
this.proxiesModulesLoading = undefined;
Object.assign(this, functions);
//
// if (isNode) {
// this.nodeVersion = process.version.match (/\d+\.\d+\.\d+/)[0]
// this.userAgent = {
// 'User-Agent': 'ccxt/' + (Exchange as any).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; // treat numbers in json as quoted precise strings
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;
this.twofa = undefined; // two-factor authentication (2FA)
// default credentials
this.apiKey = undefined;
this.secret = undefined;
this.uid = undefined;
this.login = undefined;
this.password = undefined;
this.privateKey = undefined; // a "0x"-prefixed hexstring private key for a wallet
this.walletAddress = undefined; // a wallet address "0x"-prefixed hexstring
this.token = undefined; // reserved for HTTP auth in some cases
// placeholders for cached data
this.balance = {};
this.orderbooks = {};
this.tickers = {};
this.liquidations = {};
this.orders = undefined;
this.trades = {};
this.transactions = {};
this.ohlcvs = {};
this.myLiquidations = {};
this.myTrades = undefined;
this.positions = undefined;
// web3 and cryptography flags
this.requiresWeb3 = false;
this.requiresEddsa = false;
// response handling flags and properties
this.lastRestRequestTimestamp = 0;
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;
// 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;
}
}
// http client options
const agentOptions = {
'keepAlive': true,
};
// ssl options
if (!this.validateServerSsl) {
agentOptions['rejectUnauthorized'] = false;
}
// 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);
}
}
encodeURIComponent(...args) {
// @ts-expect-error
return encodeURIComponent(...args);
}
checkRequiredVersion(requiredVersion, error = true) {
let result = true;
const [major1, minor1, patch1] = requiredVersion.split('.'), [major2, minor2, patch2] = Exchange.ccxtVersion.split('.'), intMajor1 = this.parseToInt(major1), intMinor1 = this.parseToInt(minor1), intPatch1 = this.parseToInt(patch1), intMajor2 = this.parseToInt(major2), intMinor2 = this.parseToInt(minor2), intPatch2 = this.parseToInt(patch2);
if (intMajor1 > intMajor2) {
result = false;
}
if (intMajor1 === intMajor2) {
if (intMinor1 > intMinor2) {
result = false;
}
else if (intMinor1 === intMinor2 && intPatch1 > intPatch2) {
result = false;
}
}
if (!result) {
if (error) {
throw new NotSupported('Your current version of CCXT is ' + Exchange.ccxtVersion + ', a newer version ' + requiredVersion + ' is required, please, upgrade your version of CCXT');
}
else {
return error;
}
}
return result;
}
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 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 () => {
// we have to handle it with below nested way, because of dynamic
// import issues (https://github.com/ccxt/ccxt/pull/20687)
try {
// todo: possible sync alternatives: https://stackoverflow.com/questions/51069002/convert-import-to-synchronous
this.httpProxyAgentModule = await import(/* webpackIgnore: true */ '../static_dependencies/proxies/http-proxy-agent/index.js');
this.httpsProxyAgentModule = await import(/* webpackIgnore: true */ '../static_dependencies/proxies/https-proxy-agent/index.js');
}
catch (e) {
// if several users are using those frameworks which cause exceptions,
// let them to be able to load modules still, by installing them
try {
// @ts-ignore
this.httpProxyAgentModule = await import(/* webpackIgnore: true */ 'http-proxy-agent');
// @ts-ignore
this.httpsProxyAgentModule = await import(/* webpackIgnore: true */ 'https-proxy-agent');
}
catch (e) { }
}
if (this.socksProxyAgentModuleChecked === false) {
try {
// @ts-ignore
this.socksProxyAgentModule = await import(/* webpackIgnore: true */ 'socks-proxy-agent');
}
catch (e) { }
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 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 NotSupported(this.id + ' you need to load JS proxy modules with `await instance.loadProxyModules()` method at first to use proxies');
}
if (!(httpProxy in this.proxyDictionaries)) {
this.proxyDictionaries[httpProxy] = new this.httpProxyAgentModule.HttpProxyAgent(httpProxy);
}
chosenAgent = this.proxyDictionaries[httpProxy];
}
else if (httpsProxy) {
if (this.httpsProxyAgentModule === undefined) {
throw new NotSupported(this.id + ' you need to load JS proxy modules with `await instance.loadProxyModules()` method at first to use proxies');
}
if (!(httpsProxy in this.proxyDictionaries)) {
this.proxyDictionaries[httpsProxy] = new this.httpsProxyAgentModule.HttpsProxyAgent(httpsProxy);
}
chosenAgent = this.proxyDictionaries[httpsProxy];
chosenAgent.keepAlive = true;
}
else if (socksProxy) {
if (this.socksProxyAgentModule === undefined) {
throw new 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');
}
if (!(socksProxy in this.proxyDictionaries)) {
this.proxyDictionaries[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 import(/* 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 NotSupported(this.id + ' to use proxy with non-ssl ws:// urls, at first run `await exchange.loadHttpProxyAgent()` method');
}
return this.httpAgent;
}
}
return undefined;
}
async fetch(url, method = 'GET', headers = undefined, body = undefined) {
// load node-http(s) modules only on first call
if (isNode) {
if (!this.nodeHttpModuleLoaded) {
this.nodeHttpModuleLoaded = true;
const httpsModule = await import(/* webpackIgnore: true */ 'node:https');
this.httpsAgent = new httpsModule.Agent({ keepAlive: true });
}
}
// ##### PROXY & HEADERS #####
headers = this.extend(this.headers, headers);
// proxy-url
const proxyUrl = this.checkProxyUrlSettings(url, method, headers, body);
let httpProxyAgent = false;
if (proxyUrl !== undefined) {
// part only for node-js
if (isNode) {
// in node-js we need to set header to *
headers = this.extend({ 'Origin': this.origin }, headers);
// only for http proxy
if (proxyUrl.substring(0, 5) === 'http:') {
await this.loadHttpProxyAgent();
httpProxyAgent = this.httpAgent;
}
}
url = proxyUrl + this.urlEncoderForProxyUrl(url);
}
// proxy agents
const [httpProxy, httpsProxy, socksProxy] = this.checkProxySettings(url, method, headers, body);
this.checkConflictingProxies(httpProxy || httpsProxy || socksProxy, proxyUrl);
// skip proxies on the browser
if (isNode) {
// this is needed in JS, independently whether proxy properties were set or not, we have to load them because of necessity in WS, which would happen beyond 'fetch' method (WS/etc)
await this.loadProxyModules();
}
const chosenAgent = this.setProxyAgents(httpProxy, httpsProxy, socksProxy);
// user-agent
const userAgent = (this.userAgent !== undefined) ? this.userAgent : this.user_agent;
if (userAgent && isNode) {
if (typeof userAgent === 'string') {
headers = this.extend({ 'User-Agent': userAgent }, headers);
}
else if ((typeof userAgent === 'object') && ('User-Agent' in userAgent)) {
headers = this.extend(userAgent, headers);
}
}
// set final headers
headers = this.setHeaders(headers);
// log
if (this.verbose) {
this.log("fetch Request:\n", this.id, method, url, "\nRequestHeaders:\n", headers, "\nRequestBody:\n", body, "\n");
}
// end of proxies & headers
if (this.fetchImplementation === undefined) {
if (isNode) {
if (this.agent === undefined) {
this.agent = this.httpsAgent;
}
try {
const module = await import(/* webpackIgnore: true */ '../static_dependencies/node-fetch/index.js');
this.AbortError = module.AbortError;
this.fetchImplementation = module.default;
this.FetchError = module.FetchError;
}
catch (e) {
// some users having issues with dynamic imports (https://github.com/ccxt/ccxt/pull/20687)
// so let them to fallback to node's native fetch
if (typeof fetch === 'function') {
this.fetchImplementation = fetch;
// as it's browser-compatible implementation ( https://nodejs.org/dist/latest-v20.x/docs/api/globals.html#fetch )
// it throws same error types
this.AbortError = DOMException;
this.FetchError = TypeError;
}
else {
throw new Error('Seems, "fetch" function is not available in your node-js version, please use latest node-js version');
}
}
}
else {
this.fetchImplementation = (selfIsDefined()) ? self.fetch : fetch;
this.AbortError = DOMException;
this.FetchError = TypeError;
}
}
// fetchImplementation cannot be called on this. in browsers:
// TypeError Failed to execute 'fetch' on 'Window': Illegal invocation
const fetchImplementation = this.fetchImplementation;
const params = { method, headers, body, timeout: this.timeout };
if (this.agent) {
params['agent'] = this.agent;
}
// override agent, if needed
if (httpProxyAgent) {
// if proxyUrl is being used, then specifically in nodejs, we need http module, not https
params['agent'] = httpProxyAgent;
}
else if (chosenAgent) {
// if http(s)Proxy is being used
params['agent'] = chosenAgent;
}
const controller = new AbortController();
params['signal'] = controller.signal;
const timeout = setTimeout(() => {
controller.abort();
}, this.timeout);
try {
const response = await fetchImplementation(url, params);
clearTimeout(timeout);
return this.handleRestResponse(response, url, method, headers, body);
}
catch (e) {
if (e instanceof this.AbortError) {
throw new RequestTimeout(this.id + ' ' + method + ' ' + url + ' request timed out (' + this.timeout + ' ms)');
}
else if (e instanceof this.FetchError) {
throw new NetworkError(this.id + ' ' + method + ' ' + url + ' fetch failed');
}
throw e;
}
}
parseJson(jsonString) {
try {
if (this.isJsonEncodedObject(jsonString)) {
return JSON.parse(this.onJsonResponse(jsonString));
}
}
catch (e) {
// SyntaxError
return undefined;
}
}
getResponseHeaders(response) {
const result = {};
response.headers.forEach((value, key) => {
key = key.split('-').map((word) => this.capitalize(word)).join('-');
result[key] = value;
});
return result;
}
handleRestResponse(response, url, method = 'GET', requestHeaders = undefined, requestBody = undefined) {
const responseHeaders = this.getResponseHeaders(response);
if (this.handleContentTypeApplicationZip && (responseHeaders['Content-Type'] === 'application/zip')) {
const responseBuffer = response.buffer();
if (this.enableLastResponseHeaders) {
this.last_response_headers = responseHeaders;
}
if (this.enableLastHttpResponse) {
this.last_http_response = responseBuffer;
}
if (this.verbose) {
this.log("handleRestResponse:\n", this.id, method, url, response.status, response.statusText, "\nResponseHeaders:\n", responseHeaders, "ZIP redacted", "\n");
}
// no error handler needed, because it would not be a zip response in case of an error
return responseBuffer;
}
return response.text().then((responseBody) => {
const bodyText = this.onRestResponse(response.status, response.statusText, url, method, responseHeaders, responseBody, requestHeaders, requestBody);
const json = this.parseJson(bodyText);
if (this.enableLastResponseHeaders) {
this.last_response_headers = responseHeaders;
}
if (this.enableLastHttpResponse) {
this.last_http_response = responseBody;
}
if (this.enableLastJsonResponse) {
this.last_json_response = json;
}
if (this.verbose) {
this.log("handleRestResponse:\n", this.id, method, url, response.status, response.statusText, "\nResponseHeaders:\n", responseHeaders, "\nResponseBody:\n", responseBody, "\n");
}
const skipFurtherErrorHandling = this.handleErrors(response.status, response.statusText, url, method, responseHeaders, responseBody, json, requestHeaders, requestBody);
if (!skipFurtherErrorHandling) {
this.handleHttpStatusCode(response.status, response.statusText, url, method, responseBody);
}
return json || responseBody;
});
}
onRestResponse(statusCode, statusText, url, method, responseHeaders, responseBody, requestHeaders, requestBody) {
return responseBody.trim();
}
onJsonResponse(responseBody) {
return this.quoteJsonNumbers ? responseBody.replace(/":([+.0-9eE-]+)([,}])/g, '":"$1"$2') : responseBody;
}
async loadMarketsHelper(reload = false, params = {}) {
if (!reload && this.markets) {
if (!this.markets_by_id) {
return this.setMarkets(this.markets);
}
return this.markets;
}
let currencies = undefined;
// only call if exchange API provides endpoint (true), thus avoid emulated versions ('emulated')
if (this.has['fetchCurrencies'] === true) {
currencies = await this.fetchCurrencies();
this.options['cachedCurrencies'] = currencies;
}
const markets = await this.fetchMarkets(params);
if ('cachedCurrencies' in this.options) {
delete this.options['cachedCurrencies'];
}
return this.setMarkets(markets, currencies);
}
/**
* @method
* @name Exchange#loadMarkets
* @description Loads and prepares the markets for trading.
* @param {boolean} reload - If true, the markets will be reloaded from the exchange.
* @param {object} params - Additional exchange-specific parameters for the request.
* @returns A promise that resolves to a dictionary of markets.
* @throws An error if the markets cannot be loaded or prepared.
* @remarks This method is asynchronous and returns a promise.
* It ensures that the markets are only loaded once, even if the method is called multiple times.
* If the markets are already loaded and not reloading, the method returns the existing markets.
* If the markets are being reloaded, the method waits for the reload to complete before returning the markets.
* If an error occurs during the loading or preparation of the markets, the promise is rejected with the error.
*/
async loadMarkets(reload = false, params = {}) {
if ((reload && !this.reloadingMarkets) || !this.marketsLoading) {
this.reloadingMarkets = true;
this.marketsLoading = this.loadMarketsHelper(reload, params).then((resolved) => {
this.reloadingMarkets = false;
return resolved;
}, (error) => {
this.reloadingMarkets = false;
throw error;
});
}
return this.marketsLoading;
}
async fetchCurrencies(params = {}) {
// markets are returned as a list
// currencies are returned as a dict
// this is for historical reasons
// and may be changed for consistency later
return new Promise((resolve, reject) => resolve(this.currencies));
}
async fetchCurrenciesWs(params = {}) {
// markets are returned as a list
// currencies are returned as a dict
// this is for historical reasons
// and may be changed for consistency later
return new Promise((resolve, reject) => resolve(this.currencies));
}
async fetchMarkets(params = {}) {
// markets are returned as a list
// currencies are returned as a dict
// this is for historical reasons
// and may be changed for consistency later
return new Promise((resolve, reject) => resolve(Object.values(this.markets)));
}
async fetchMarketsWs(params = {}) {
// markets are returned as a list
// currencies are returned as a dict
// this is for historical reasons
// and may be changed for consistency later
return new Promise((resolve, reject) => resolve(Object.values(this.markets)));
}
checkRequiredDependencies() {
return;
}
parseNumber(value, d = undefined) {
if (value === undefined) {
return d;
}
else {
try {
// we should handle scientific notation here
// so if the exchanges returns 1e-8
// this function will return 0.00000001
// check https://github.com/ccxt/ccxt/issues/24135
const numberNormalized = this.numberToString(value);
if (numberNormalized.indexOf('e-') > -1) {
return this.number(numberToString(parseFloat(numberNormalized)));
}
const result = this.number(numberNormalized);
return isNaN(result) ? d : result;
}
catch (e) {
return d;
}
}
}
checkOrderArguments(market, type, side, amount, price, params) {
if (price === undefined) {
if (type === 'limit') {
throw new ArgumentsRequired(this.id + ' createOrder() requires a price argument for a limit order');
}
}
if (amount <= 0) {
throw new ArgumentsRequired(this.id + ' createOrder() amount should be above 0');
}
}
handleHttpStatusCode(code, reason, url, method, body) {
const codeAsString = code.toString();
if (codeAsString in this.httpExceptions) {
const ErrorClass = this.httpExceptions[codeAsString];
throw new ErrorClass(this.id + ' ' + method + ' ' + url + ' ' + codeAsString + ' ' + reason + ' ' + body);
}
}
remove0xPrefix(hexData) {
if (hexData.slice(0, 2) === '0x') {
return hexData.slice(2);
}
else {
return hexData;
}
}
mapToSafeMap(dict) {
return dict; // wrapper for go
}
safeMapToMap(dict) {
return dict; // wrapper for go
}
spawn(method, ...args) {
const future = Future();
// using setTimeout 0 to force the execution to run after the future is returned
setTimeout(() => {
method.apply(this, args).then(future.resolve).catch(future.reject);
}, 0);
return future;
}
delay(timeout, method, ...args) {
setTimeout(() => {
this.spawn(method, ...args);
}, timeout);
}
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// WS/PRO methods
orderBook(snapshot = {}, depth = Number.MAX_SAFE_INTEGER) {
return new WsOrderBook(snapshot, depth);
}
indexedOrderBook(snapshot = {}, depth = Number.MAX_SAFE_INTEGER) {
return new IndexedOrderBook(snapshot, depth);
}
countedOrderBook(snapshot = {}, depth = Number.MAX_SAFE_INTEGER) {
return new CountedOrderBook(snapshot, depth);
}
handleMessage(client, message) { } // stub to override
// ping (client: Client) {} // stub to override
ping(client) {
return undefined;
}
client(url) {
this.clients = this.clients || {};
if (!this.clients[url]) {
const onMessage = this.handleMessage.bind(this);
const onError = this.onError.bind(this);
const onClose = this.onClose.bind(this);
const onConnected = this.onConnected.bind(this);
// decide client type here: ws / signalr / socketio
const wsOptions = this.safeValue(this.options, 'ws', {});
// proxy agents
const [httpProxy, httpsProxy, socksProxy] = this.checkWsProxySettings();
const chosenAgent = this.setProxyAgents(httpProxy, httpsProxy, socksProxy);
// part only for node-js
const httpProxyAgent = this.getHttpAgentIfNeeded(url);
const finalAgent = chosenAgent ? chosenAgent : (httpProxyAgent ? httpProxyAgent : this.agent);
//
const options = this.deepExtend(this.streaming, {
'log': this.log ? this.log.bind(this) : this.log,
'ping': this.ping ? this.ping.bind(this) : this.ping,
'verbose': this.verbose,
'throttler': new Throttler(this.tokenBucket),
// add support for proxies
'options': {
'agent': finalAgent,
}
}, wsOptions);
this.clients[url] = new WsClient(url, onMessage, onError, onClose, onConnected, options);
}
return this.clients[url];
}
watchMultiple(url, messageHashes, message = undefined, subscribeHashes = undefined, subscription = undefined) {
//
// Without comments the code of this method is short and easy:
//
// const client = this.client (url)
// const backoffDelay = 0
// const future = client.future (messageHash)
// const connected = client.connect (backoffDelay)
// connected.then (() => {
// if (message && !client.subscriptions[subscribeHash]) {
// client.subscriptions[subscribeHash] = true
// client.send (message)
// }
// }).catch ((error) => {})
// return future
//
// The following is a longer version of this method with comments
//
const client = this.client(url);
// todo: calculate the backoff using the clients cache
const backoffDelay = 0;
//
// watchOrderBook ---- future ----+---------------+----→ user
// | |
// ↓ ↑
// | |
// connect ......→ resolve
// | |
// ↓ ↑
// | |
// subscribe -----→ receive
//
const future = Future.race(messageHashes.map(messageHash => client.future(messageHash)));
// read and write subscription, this is done before connecting the client
// to avoid race conditions when other parts of the code read or write to the client.subscriptions
let missingSubscriptions = [];
if (subscribeHashes !== undefined) {
for (let i = 0; i < subscribeHashes.length; i++) {
const subscribeHash = subscribeHashes[i];
if (!client.subscriptions[subscribeHash]) {
missingSubscriptions.push(subscribeHash);
client.subscriptions[subscribeHash] = subscription || true;
}
}
}
// we intentionally do not use await here to avoid unhandled exceptions
// the policy is to make sure that 100% of promises are resolved or rejected
// either with a call to client.resolve or client.reject with
// a proper exception class instance
const connected = client.connect(backoffDelay);
// the following is executed only if the catch-clause does not
// catch any connection-level exceptions from the client
// (connection established successfully)
if ((subscribeHashes === undefined) || missingSubscriptions.length) {
connected.then(() => {
const options = this.safeValue(this.options, 'ws');
const cost = this.safeValue(options, 'cost', 1);
if (message) {
if (this.enableRateLimit && client.throttle) {
// add cost here |
// |
// V
client.throttle(cost).then(() => {
client.send(message);
}).catch((e) => {
for (let i = 0; i < missingSubscriptions.length; i++) {
const subscribeHash = missingSubscriptions[i];
delete client.subscriptions[subscribeHash];
}
future.reject(e);
});
}
else {
client.send(message)
.catch((e) => {
for (let i = 0; i < missingSubscriptions.length; i++) {
const subscribeHash = missingSubscriptions[i];
delete client.subscriptions[subscribeHash];
}
future.reject(e);
});
}
}
}).catch((e) => {
for (let i = 0; i < missingSubscriptions.length; i++) {
const subscribeHash = missingSubscriptions[i];
delete client.subscriptions[subscribeHash];
}
future.reject(e);
});
}
return future;
}
watch(url, messageHash, message = undefined, subscribeHash = undefined, subscription = undefined) {
//
// Without comments the code of this method is short and easy:
//
// const client = this.client (url)
// const backoffDelay = 0
// const future = client.future (messageHash)
// const connected = client.connect (backoffDelay)
// connected.then (() => {
// if (message && !client.subscriptions[subscribeHash]) {
// client.subscriptions[subscribeHash] = true
// client.send (message)
// }
// }).catch ((error) => {})
// return future
//
// The following is a longer version of this method with comments
//
const client = this.client(url);
// todo: calculate the backoff using the clients cache
const backoffDelay = 0;
//
// watchOrderBook ---- future ----+---------------+----→ user
// | |
// ↓ ↑
// | |
// connect ......→ resolve
// | |
// ↓ ↑
// | |
// subscribe -----→ receive
//
if ((subscribeHash === undefined) && (messageHash in client.futures)) {
return client.futures[messageHash];
}
const future = client.future(messageHash);
// read and write subscription, this is done before connecting the client
// to avoid race conditions when other parts of the code read or write to the client.subscriptions