sfccxt
Version:
A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges
1,098 lines (1,050 loc) • 136 kB
JavaScript
'use strict';
// ----------------------------------------------------------------------------
/* eslint-disable */
const functions = require ('./functions');
const {
isNode
, clone
, unCamelCase
, throttle
, timeout
, TimedOut
, defaultFetch
} = functions
const { // eslint-disable-line object-curly-newline
ExchangeError
, BadSymbol
, NullResponse
, InvalidAddress
, InvalidOrder
, NotSupported
, AuthenticationError
, DDoSProtection
, RequestTimeout
, ExchangeNotAvailable
, RateLimitExceeded
, ArgumentsRequired } = require ('./errors')
const { TRUNCATE, ROUND, DECIMAL_PLACES, NO_PADDING, TICK_SIZE } = functions.precisionConstants
const BN = require ('../static_dependencies/BN/bn')
const Precise = require ('./Precise')
// ----------------------------------------------------------------------------
module.exports = class Exchange {
describe () {
return {
'id': undefined,
'name': undefined,
'countries': undefined,
'enableRateLimit': true,
'rateLimit': 2000, // milliseconds = seconds * 1000
'certified': false, // if certified by the CCXT dev team
'pro': false, // if it is integrated with CCXT Pro for WebSocket support
'alias': false, // whether this exchange is an alias to another exchange
'has': {
'publicAPI': true,
'privateAPI': true,
'CORS': undefined,
'spot': undefined,
'margin': undefined,
'swap': undefined,
'future': undefined,
'option': undefined,
'addMargin': undefined,
'cancelAllOrders': undefined,
'cancelOrder': true,
'cancelOrders': undefined,
'createDepositAddress': undefined,
'createLimitOrder': true,
'createMarketOrder': true,
'createOrder': true,
'createPostOnlyOrder': undefined,
'createReduceOnlyOrder': undefined,
'createStopOrder': undefined,
'createStopLimitOrder': undefined,
'createStopMarketOrder': undefined,
'editOrder': 'emulated',
'fetchAccounts': undefined,
'fetchBalance': true,
'fetchBidsAsks': undefined,
'fetchBorrowInterest': undefined,
'fetchBorrowRate': undefined,
'fetchBorrowRateHistory': undefined,
'fetchBorrowRatesPerSymbol': undefined,
'fetchBorrowRates': undefined,
'fetchCanceledOrders': undefined,
'fetchClosedOrder': undefined,
'fetchClosedOrders': undefined,
'fetchCurrencies': 'emulated',
'fetchDeposit': undefined,
'fetchDepositAddress': undefined,
'fetchDepositAddresses': undefined,
'fetchDepositAddressesByNetwork': undefined,
'fetchDeposits': undefined,
'fetchTransactionFee': undefined,
'fetchTransactionFees': undefined,
'fetchFundingHistory': undefined,
'fetchFundingRate': undefined,
'fetchFundingRateHistory': undefined,
'fetchFundingRates': undefined,
'fetchIndexOHLCV': undefined,
'fetchL2OrderBook': true,
'fetchLedger': undefined,
'fetchLedgerEntry': undefined,
'fetchLeverageTiers': undefined,
'fetchMarketLeverageTiers': undefined,
'fetchMarkets': true,
'fetchMarkOHLCV': undefined,
'fetchMyTrades': undefined,
'fetchOHLCV': 'emulated',
'fetchOpenInterest': undefined,
'fetchOpenInterestHistory': undefined,
'fetchOpenOrder': undefined,
'fetchOpenOrders': undefined,
'fetchOrder': undefined,
'fetchOrderBook': true,
'fetchOrderBooks': undefined,
'fetchOrders': undefined,
'fetchOrderTrades': undefined,
'fetchPermissions': undefined,
'fetchPosition': undefined,
'fetchPositions': undefined,
'fetchPositionsRisk': undefined,
'fetchPremiumIndexOHLCV': undefined,
'fetchStatus': 'emulated',
'fetchTicker': true,
'fetchTickers': undefined,
'fetchTime': undefined,
'fetchTrades': true,
'fetchTradingFee': undefined,
'fetchTradingFees': undefined,
'fetchTradingLimits': undefined,
'fetchTransactions': undefined,
'fetchTransfers': undefined,
'fetchWithdrawAddresses': undefined,
'fetchWithdrawal': undefined,
'fetchWithdrawals': undefined,
'reduceMargin': undefined,
'setLeverage': undefined,
'setMargin': undefined,
'setMarginMode': undefined,
'setPositionMode': undefined,
'signIn': undefined,
'transfer': undefined,
'withdraw': undefined,
},
'urls': {
'logo': undefined,
'api': undefined,
'www': undefined,
'doc': undefined,
'fees': undefined,
},
'api': undefined,
'requiredCredentials': {
'apiKey': true,
'secret': true,
'uid': false,
'login': false,
'password': false,
'twofa': false, // 2-factor authentication (one-time password key)
'privateKey': false, // a "0x"-prefixed hexstring private key for a wallet
'walletAddress': false, // the wallet address "0x"-prefixed hexstring
'token': false, // reserved for HTTP auth in some cases
},
'markets': undefined, // to be filled manually or by fetchMarkets
'currencies': {}, // to be filled manually or by fetchMarkets
'timeframes': undefined, // redefine if the exchange has.fetchOHLCV
'fees': {
'trading': {
'tierBased': undefined,
'percentage': undefined,
'taker': undefined,
'maker': undefined,
},
'funding': {
'tierBased': undefined,
'percentage': undefined,
'withdraw': {},
'deposit': {},
},
},
'status': {
'status': 'ok',
'updated': undefined,
'eta': undefined,
'url': undefined,
},
'exceptions': undefined,
'httpExceptions': {
'422': ExchangeError,
'418': DDoSProtection,
'429': RateLimitExceeded,
'404': ExchangeNotAvailable,
'409': ExchangeNotAvailable,
'410': ExchangeNotAvailable,
'451': ExchangeNotAvailable,
'500': ExchangeNotAvailable,
'501': ExchangeNotAvailable,
'502': ExchangeNotAvailable,
'520': ExchangeNotAvailable,
'521': ExchangeNotAvailable,
'522': ExchangeNotAvailable,
'525': ExchangeNotAvailable,
'526': ExchangeNotAvailable,
'400': ExchangeNotAvailable,
'403': ExchangeNotAvailable,
'405': ExchangeNotAvailable,
'503': ExchangeNotAvailable,
'530': ExchangeNotAvailable,
'408': RequestTimeout,
'504': RequestTimeout,
'401': AuthenticationError,
'407': AuthenticationError,
'511': AuthenticationError,
},
'commonCurrencies': { // gets extended/overwritten in subclasses
'XBT': 'BTC',
'BCC': 'BCH',
'BCHABC': 'BCH',
'BCHSV': 'BSV',
},
'precisionMode': DECIMAL_PLACES,
'paddingMode': NO_PADDING,
'limits': {
'leverage': { 'min': undefined, 'max': undefined },
'amount': { 'min': undefined, 'max': undefined },
'price': { 'min': undefined, 'max': undefined },
'cost': { 'min': undefined, 'max': undefined },
},
} // return
} // describe ()
constructor (userConfig = {}) {
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)
this.fetchOptions = {
// keepalive: true, // does not work in Chrome, https://github.com/ccxt/ccxt/issues/6368
}
// http properties
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 = {}
// prepended to URL, like https://proxy.com/https://exchange.com/api...
this.proxy = ''
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 = defaultFetch
this.validateServerSsl = true
this.validateClientSsl = false
// default property values
this.timeout = 10000 // milliseconds
this.verbose = false
this.debug = false
this.userAgent = undefined
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.orders = undefined
this.trades = {}
this.transactions = {}
this.ohlcvs = {}
this.myTrades = undefined
this.positions = {}
// web3 and cryptography flags
this.requiresWeb3 = false
this.requiresEddsa = false
this.precision = {}
// response handling flags and properties
this.lastRestRequestTimestamp = 0
this.enableLastJsonResponse = true
this.enableLastHttpResponse = true
this.enableLastResponseHeaders = true
this.last_http_response = undefined
this.last_json_response = undefined
this.last_response_headers = 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;
}
// js-specific http options
if (!this.httpAgent && defaultFetch.http && isNode) {
this.httpAgent = new defaultFetch.http.Agent (agentOptions)
}
if (!this.httpsAgent && defaultFetch.https && isNode) {
this.httpsAgent = new defaultFetch.https.Agent (agentOptions)
}
// 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')
}
// init the request rate limiter
this.initRestRateLimiter ()
// init predefined markets if any
if (this.markets) {
this.setMarkets (this.markets)
}
}
encodeURIComponent (...args) {
return encodeURIComponent (...args)
}
checkRequiredVersion (requiredVersion, error = true) {
let result = true
const [ major1, minor1, patch1 ] = requiredVersion.split ('.')
, [ major2, minor2, patch2 ] = Exchange.ccxtVersion.split ('.')
, intMajor1 = parseInt (major1)
, intMinor1 = parseInt (minor1)
, intPatch1 = parseInt (patch1)
, intMajor2 = parseInt (major2)
, intMinor2 = parseInt (minor2)
, intPatch2 = parseInt (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
}
checkAddress (address) {
if (address === undefined) {
throw new InvalidAddress (this.id + ' address is undefined')
}
// check the address is not the same letter like 'aaaaa' nor too short nor has a space
if ((this.unique (address).length === 1) || address.length < this.minFundingAddressLength || address.includes (' ')) {
throw new InvalidAddress (this.id + ' address is invalid or has less than ' + this.minFundingAddressLength.toString () + ' characters: "' + this.json (address) + '"')
}
return address
}
initRestRateLimiter () {
if (this.rateLimit === undefined) {
throw new Error (this.id + '.rateLimit property is not configured');
}
this.tokenBucket = this.extend ({
delay: 0.001,
capacity: 1,
cost: 1,
maxCapacity: 1000,
refillRate: (this.rateLimit > 0) ? 1 / this.rateLimit : Number.MAX_VALUE,
}, this.tokenBucket);
this.throttle = throttle (this.tokenBucket);
this.executeRestRequest = (url, method = 'GET', headers = undefined, body = undefined) => {
// 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;
} else if (this.httpAgent && url.indexOf ('http://') === 0) {
params['agent'] = this.httpAgent;
} else if (this.httpsAgent && url.indexOf ('https://') === 0) {
params['agent'] = this.httpsAgent;
}
const promise =
fetchImplementation (url, this.extend (params, this.fetchOptions))
.catch ((e) => {
if (isNode) {
throw new ExchangeNotAvailable ([ this.id, method, url, e.type, e.message ].join (' '));
}
throw e; // rethrow all unknown errors
})
.then ((response) => this.handleRestResponse (response, url, method, headers, body));
return timeout (this.timeout, promise).catch ((e) => {
if (e instanceof TimedOut) {
throw new RequestTimeout (this.id + ' ' + method + ' ' + url + ' request timed out (' + this.timeout + ' ms)');
}
throw e;
})
};
}
setSandboxMode (enabled) {
if (!!enabled) { // eslint-disable-line no-extra-boolean-cast
if ('test' in this.urls) {
if (typeof this.urls['api'] === 'string') {
this.urls['apiBackup'] = this.urls['api']
this.urls['api'] = this.urls['test']
} else {
this.urls['apiBackup'] = clone (this.urls['api'])
this.urls['api'] = clone (this.urls['test'])
}
} else {
throw new NotSupported (this.id + ' does not have a sandbox URL')
}
} else if ('apiBackup' in this.urls) {
if (typeof this.urls['api'] === 'string') {
this.urls['api'] = this.urls['apiBackup']
} else {
this.urls['api'] = clone (this.urls['apiBackup'])
}
}
}
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)
}
fetch (url, method = 'GET', headers = undefined, body = undefined) {
if (isNode && this.userAgent) {
if (typeof this.userAgent === 'string') {
headers = this.extend ({ 'User-Agent': this.userAgent }, headers)
} else if ((typeof this.userAgent === 'object') && ('User-Agent' in this.userAgent)) {
headers = this.extend (this.userAgent, headers)
}
}
if (typeof this.proxy === 'function') {
url = this.proxy (url)
if (isNode) {
headers = this.extend ({ 'Origin': this.origin }, headers)
}
} else if (typeof this.proxy === 'string') {
if (this.proxy.length && isNode) {
headers = this.extend ({ 'Origin': this.origin }, headers)
}
url = this.proxy + url
}
headers = this.extend (this.headers, headers)
headers = this.setHeaders (headers)
if (this.verbose) {
this.log ("fetch Request:\n", this.id, method, url, "\nRequestHeaders:\n", headers, "\nRequestBody:\n", body, "\n")
}
return this.executeRestRequest (url, method, headers, body)
}
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 ()
}
const markets = await this.fetchMarkets (params)
return this.setMarkets (markets, currencies)
}
loadMarkets (reload = false, params = {}) {
// this method is async, it returns a promise
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
}
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));
}
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)))
}
filterBySinceLimit (array, since = undefined, limit = undefined, key = 'timestamp', tail = false) {
const sinceIsDefined = (since !== undefined && since !== null)
if (sinceIsDefined) {
array = array.filter ((entry) => entry[key] >= since)
}
if (limit !== undefined && limit !== null) {
array = tail ? array.slice (-limit) : array.slice (0, limit)
}
return array
}
filterByValueSinceLimit (array, field, value = undefined, since = undefined, limit = undefined, key = 'timestamp', tail = false) {
const valueIsDefined = value !== undefined && value !== null
const sinceIsDefined = since !== undefined && since !== null
// single-pass filter for both symbol and since
if (valueIsDefined || sinceIsDefined) {
array = array.filter ((entry) =>
((valueIsDefined ? (entry[field] === value) : true) &&
(sinceIsDefined ? (entry[key] >= since) : true)))
}
if (limit !== undefined && limit !== null) {
array = tail ? array.slice (-limit) : array.slice (0, limit)
}
return array
}
checkRequiredDependencies () {
return
}
remove0xPrefix (hexData) {
if (hexData.slice (0, 2) === '0x') {
return hexData.slice (2)
} else {
return hexData
}
}
hashMessage (message) {
// takes a hex encoded message
const binaryMessage = this.base16ToBinary (this.remove0xPrefix (message))
const prefix = this.stringToBinary ('\x19Ethereum Signed Message:\n' + binaryMessage.sigBytes)
return '0x' + this.hash (this.binaryConcat (prefix, binaryMessage), 'keccak', 'hex')
}
signHash (hash, privateKey) {
const signature = this.ecdsa (hash.slice (-64), privateKey.slice (-64), 'secp256k1', undefined)
return {
'r': '0x' + signature['r'],
's': '0x' + signature['s'],
'v': 27 + signature['v'],
}
}
signMessage (message, privateKey) {
return this.signHash (this.hashMessage (message), privateKey.slice (-64))
}
signMessageString (message, privateKey) {
// still takes the input as a hex string
// same as above but returns a string instead of an object
const signature = this.signMessage (message, privateKey)
return signature['r'] + this.remove0xPrefix (signature['s']) + this.binaryToBase16 (this.numberToBE (signature['v']))
}
parseNumber (value, d = undefined) {
if (value === undefined) {
return d
} else {
try {
return this.number (value)
} 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);
}
}
/* eslint-enable */
// ------------------------------------------------------------------------
// ########################################################################
// ########################################################################
// ########################################################################
// ########################################################################
// ######## ######## ########
// ######## ######## ########
// ######## ######## ########
// ######## ######## ########
// ######## ######################## ########################
// ######## ######################## ########################
// ######## ######################## ########################
// ######## ######################## ########################
// ######## ######## ########
// ######## ######## ########
// ######## ######## ########
// ######## ######## ########
// ########################################################################
// ########################################################################
// ########################################################################
// ########################################################################
// ######## ######## ######## ########
// ######## ######## ######## ########
// ######## ######## ######## ########
// ######## ######## ######## ########
// ################ ######################## ################
// ################ ######################## ################
// ################ ######################## ################
// ################ ######################## ################
// ######## ######## ################ ################
// ######## ######## ################ ################
// ######## ######## ################ ################
// ######## ######## ################ ################
// ########################################################################
// ########################################################################
// ########################################################################
// ########################################################################
// ------------------------------------------------------------------------
// METHODS BELOW THIS LINE ARE TRANSPILED FROM JAVASCRIPT TO PYTHON AND PHP
getDefaultOptions () {
return {
'defaultNetworkCodeReplacements': {
'ETH': { 'ERC20': 'ETH' },
'TRX': { 'TRC20': 'TRX' },
'CRO': { 'CRC20': 'CRONOS' },
},
};
}
safeLedgerEntry (entry, currency = undefined) {
currency = this.safeCurrency (undefined, currency);
let direction = this.safeString (entry, 'direction');
let before = this.safeString (entry, 'before');
let after = this.safeString (entry, 'after');
const amount = this.safeString (entry, 'amount');
if (amount !== undefined) {
if (before === undefined && after !== undefined) {
before = Precise.stringSub (after, amount);
} else if (before !== undefined && after === undefined) {
after = Precise.stringAdd (before, amount);
}
}
if (before !== undefined && after !== undefined) {
if (direction === undefined) {
if (Precise.stringGt (before, after)) {
direction = 'out';
}
if (Precise.stringGt (after, before)) {
direction = 'in';
}
}
}
const fee = this.safeValue (entry, 'fee');
if (fee !== undefined) {
fee['cost'] = this.safeNumber (fee, 'cost');
}
const timestamp = this.safeInteger (entry, 'timestamp');
return {
'id': this.safeString (entry, 'id'),
'timestamp': timestamp,
'datetime': this.iso8601 (timestamp),
'direction': direction,
'account': this.safeString (entry, 'account'),
'referenceId': this.safeString (entry, 'referenceId'),
'referenceAccount': this.safeString (entry, 'referenceAccount'),
'type': this.safeString (entry, 'type'),
'currency': currency['code'],
'amount': this.parseNumber (amount),
'before': this.parseNumber (before),
'after': this.parseNumber (after),
'status': this.safeString (entry, 'status'),
'fee': fee,
'info': entry,
};
}
setMarkets (markets, currencies = undefined) {
const values = [];
const marketValues = this.toArray (markets);
for (let i = 0; i < marketValues.length; i++) {
const market = this.deepExtend (this.safeMarket (), {
'precision': this.precision,
'limits': this.limits,
}, this.fees['trading'], marketValues[i]);
values.push (market);
}
this.markets = this.indexBy (values, 'symbol');
this.markets_by_id = this.indexBy (markets, 'id');
const marketsSortedBySymbol = this.keysort (this.markets);
const marketsSortedById = this.keysort (this.markets_by_id);
this.symbols = Object.keys (marketsSortedBySymbol);
this.ids = Object.keys (marketsSortedById);
if (currencies !== undefined) {
this.currencies = this.deepExtend (this.currencies, currencies);
} else {
let baseCurrencies = [];
let quoteCurrencies = [];
for (let i = 0; i < values.length; i++) {
const market = values[i];
const defaultCurrencyPrecision = (this.precisionMode === DECIMAL_PLACES) ? 8 : this.parseNumber ('1e-8');
const marketPrecision = this.safeValue (market, 'precision', {});
if ('base' in market) {
const currencyPrecision = this.safeValue2 (marketPrecision, 'base', 'amount', defaultCurrencyPrecision);
const currency = {
'id': this.safeString2 (market, 'baseId', 'base'),
'numericId': this.safeString (market, 'baseNumericId'),
'code': this.safeString (market, 'base'),
'precision': currencyPrecision,
};
baseCurrencies.push (currency);
}
if ('quote' in market) {
const currencyPrecision = this.safeValue2 (marketPrecision, 'quote', 'price', defaultCurrencyPrecision);
const currency = {
'id': this.safeString2 (market, 'quoteId', 'quote'),
'numericId': this.safeString (market, 'quoteNumericId'),
'code': this.safeString (market, 'quote'),
'precision': currencyPrecision,
};
quoteCurrencies.push (currency);
}
}
baseCurrencies = this.sortBy (baseCurrencies, 'code');
quoteCurrencies = this.sortBy (quoteCurrencies, 'code');
this.baseCurrencies = this.indexBy (baseCurrencies, 'code');
this.quoteCurrencies = this.indexBy (quoteCurrencies, 'code');
const allCurrencies = this.arrayConcat (baseCurrencies, quoteCurrencies);
const groupedCurrencies = this.groupBy (allCurrencies, 'code');
const codes = Object.keys (groupedCurrencies);
const resultingCurrencies = [];
for (let i = 0; i < codes.length; i++) {
const code = codes[i];
const groupedCurrenciesCode = this.safeValue (groupedCurrencies, code, []);
let highestPrecisionCurrency = this.safeValue (groupedCurrenciesCode, 0);
for (let j = 1; j < groupedCurrenciesCode.length; j++) {
const currentCurrency = groupedCurrenciesCode[j];
if (this.precisionMode === TICK_SIZE) {
highestPrecisionCurrency = (currentCurrency['precision'] < highestPrecisionCurrency['precision']) ? currentCurrency : highestPrecisionCurrency;
} else {
highestPrecisionCurrency = (currentCurrency['precision'] > highestPrecisionCurrency['precision']) ? currentCurrency : highestPrecisionCurrency;
}
}
resultingCurrencies.push (highestPrecisionCurrency);
}
const sortedCurrencies = this.sortBy (resultingCurrencies, 'code');
this.currencies = this.deepExtend (this.currencies, this.indexBy (sortedCurrencies, 'code'));
}
this.currencies_by_id = this.indexBy (this.currencies, 'id');
const currenciesSortedByCode = this.keysort (this.currencies);
this.codes = Object.keys (currenciesSortedByCode);
return this.markets;
}
safeBalance (balance) {
const balances = this.omit (balance, [ 'info', 'timestamp', 'datetime', 'free', 'used', 'total' ]);
const codes = Object.keys (balances);
balance['free'] = {};
balance['used'] = {};
balance['total'] = {};
const debtBalance = {};
for (let i = 0; i < codes.length; i++) {
const code = codes[i];
let total = this.safeString (balance[code], 'total');
let free = this.safeString (balance[code], 'free');
let used = this.safeString (balance[code], 'used');
const debt = this.safeString (balance[code], 'debt');
if ((total === undefined) && (free !== undefined) && (used !== undefined)) {
total = Precise.stringAdd (free, used);
}
if ((free === undefined) && (total !== undefined) && (used !== undefined)) {
free = Precise.stringSub (total, used);
}
if ((used === undefined) && (total !== undefined) && (free !== undefined)) {
used = Precise.stringSub (total, free);
}
balance[code]['free'] = this.parseNumber (free);
balance[code]['used'] = this.parseNumber (used);
balance[code]['total'] = this.parseNumber (total);
balance['free'][code] = balance[code]['free'];
balance['used'][code] = balance[code]['used'];
balance['total'][code] = balance[code]['total'];
if (debt !== undefined) {
balance[code]['debt'] = this.parseNumber (debt);
debtBalance[code] = balance[code]['debt'];
}
}
const debtBalanceArray = Object.keys (debtBalance);
const length = debtBalanceArray.length;
if (length) {
balance['debt'] = debtBalance;
}
return balance;
}
safeOrder (order, market = undefined) {
// parses numbers as strings
// it is important pass the trades as unparsed rawTrades
let amount = this.omitZero (this.safeString (order, 'amount'));
let remaining = this.safeString (order, 'remaining');
let filled = this.safeString (order, 'filled');
let cost = this.safeString (order, 'cost');
let average = this.omitZero (this.safeString (order, 'average'));
let price = this.omitZero (this.safeString (order, 'price'));
let lastTradeTimeTimestamp = this.safeInteger (order, 'lastTradeTimestamp');
const parseFilled = (filled === undefined);
const parseCost = (cost === undefined);
const parseLastTradeTimeTimestamp = (lastTradeTimeTimestamp === undefined);
const fee = this.safeValue (order, 'fee');
const parseFee = (fee === undefined);
const parseFees = this.safeValue (order, 'fees') === undefined;
const shouldParseFees = parseFee || parseFees;
const fees = this.safeValue (order, 'fees', []);
let trades = [];
if (parseFilled || parseCost || shouldParseFees) {
const rawTrades = this.safeValue (order, 'trades', trades);
const oldNumber = this.number;
// we parse trades as strings here!
this.number = String;
trades = this.parseTrades (rawTrades, market, undefined, undefined, {
'symbol': order['symbol'],
'side': order['side'],
'type': order['type'],
'order': order['id'],
});
this.number = oldNumber;
let tradesLength = 0;
const isArray = Array.isArray (trades);
if (isArray) {
tradesLength = trades.length;
}
if (isArray && (tradesLength > 0)) {
// move properties that are defined in trades up into the order
if (order['symbol'] === undefined) {
order['symbol'] = trades[0]['symbol'];
}
if (order['side'] === undefined) {
order['side'] = trades[0]['side'];
}
if (order['type'] === undefined) {
order['type'] = trades[0]['type'];
}
if (order['id'] === undefined) {
order['id'] = trades[0]['order'];
}
if (parseFilled) {
filled = '0';
}
if (parseCost) {
cost = '0';
}
for (let i = 0; i < trades.length; i++) {
const trade = trades[i];
const tradeAmount = this.safeString (trade, 'amount');
if (parseFilled && (tradeAmount !== undefined)) {
filled = Precise.stringAdd (filled, tradeAmount);
}
const tradeCost = this.safeString (trade, 'cost');
if (parseCost && (tradeCost !== undefined)) {
cost = Precise.stringAdd (cost, tradeCost);
}
const tradeTimestamp = this.safeValue (trade, 'timestamp');
if (parseLastTradeTimeTimestamp && (tradeTimestamp !== undefined)) {
if (lastTradeTimeTimestamp === undefined) {
lastTradeTimeTimestamp = tradeTimestamp;
} else {
lastTradeTimeTimestamp = Math.max (lastTradeTimeTimestamp, tradeTimestamp);
}
}
if (shouldParseFees) {
const tradeFees = this.safeValue (trade, 'fees');
if (tradeFees !== undefined) {
for (let j = 0; j < tradeFees.length; j++) {
const tradeFee = tradeFees[j];
fees.push (this.extend ({}, tradeFee));
}
} else {
const tradeFee = this.safeValue (trade, 'fee');
if (tradeFee !== undefined) {
fees.push (this.extend ({}, tradeFee));
}
}
}
}
}
}
if (shouldParseFees) {
const reducedFees = this.reduceFees ? this.reduceFeesByCurrency (fees) : fees;
const reducedLength = reducedFees.length;
for (let i = 0; i < reducedLength; i++) {
reducedFees[i]['cost'] = this.safeNumber (reducedFees[i], 'cost');
if ('rate' in reducedFees[i]) {
reducedFees[i]['rate'] = this.safeNumber (reducedFees[i], 'rate');
}
}
if (!parseFee && (reducedLength === 0)) {
fee['cost'] = this.safeNumber (fee, 'cost');
if ('rate' in fee) {
fee['rate'] = this.safeNumber (fee, 'rate');
}
reducedFees.push (fee);
}
order['fees'] = reducedFees;
if (parseFee && (reducedLength === 1)) {
order['fee'] = reducedFees[0];
}
}
if (amount === undefined) {
// ensure amount = filled + remaining
if (filled !== undefined && remaining !== undefined) {
amount = Precise.stringAdd (filled, remaining);
} else if (this.safeString (order, 'status') === 'closed') {
amount = filled;