Beta 7 with some repo names changed.
This commit is contained in:
1 parent
0f7b40cca7
commit
963c19493a
3408 files changed
+462217
-15516
No files matched your search
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017,
|
||||
"sourceType": "script",
|
||||
},
|
||||
"rules": {
|
||||
"semi": "off",
|
||||
"no-unused-vars": ["off"],
|
||||
"quotes": ["off", "single"],
|
||||
"quote-props": "off",
|
||||
"func-call-spacing": ["error", "always"],
|
||||
"one-var": "off",
|
||||
"indent": ["warn", 4, { "ignoredNodes": ["ObjectPattern"] }],
|
||||
"comma-style": "off",
|
||||
"no-multi-spaces": "off",
|
||||
"comma-dangle": "off",
|
||||
"spaced-comment": "off",
|
||||
"camelcase": "off",
|
||||
"padded-blocks": "off",
|
||||
"multiline-comment-style": "off",
|
||||
"curly": "off",
|
||||
"rest-spread-spacing": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-undef-init": "off",
|
||||
"no-useless-return": "off",
|
||||
"no-console": "off",
|
||||
"operator-linebreak": "off",
|
||||
"key-spacing": "off",
|
||||
"brace-style": "off",
|
||||
"padding-line-between-statements": ["off",
|
||||
{"blankLine": "never", "prev":"*", "next": "*" },
|
||||
{"blankLine": "always", "prev":"directive", "next": "*" },
|
||||
{"blankLine": "always", "prev":"*", "next": "cjs-export" },
|
||||
],
|
||||
},
|
||||
}
|
||||
+1456
File diff suppressed because it is too large.
Load diff
+44
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
const errorHierarchy = {
|
||||
'BaseError': {
|
||||
'ExchangeError': {
|
||||
'AuthenticationError': {
|
||||
'PermissionDenied': {},
|
||||
'AccountSuspended': {},
|
||||
},
|
||||
'ArgumentsRequired': {},
|
||||
'BadRequest': {
|
||||
'BadSymbol': {},
|
||||
},
|
||||
'BadResponse': {
|
||||
'NullResponse': {},
|
||||
},
|
||||
'InsufficientFunds': {},
|
||||
'InvalidAddress': {
|
||||
'AddressPending': {},
|
||||
},
|
||||
'InvalidOrder': {
|
||||
'OrderNotFound': {},
|
||||
'OrderNotCached': {},
|
||||
'CancelPending': {},
|
||||
'OrderImmediatelyFillable': {},
|
||||
'OrderNotFillable': {},
|
||||
'DuplicateOrderId': {},
|
||||
},
|
||||
'NotSupported': {},
|
||||
},
|
||||
'NetworkError': {
|
||||
'DDoSProtection': {
|
||||
'RateLimitExceeded': {},
|
||||
},
|
||||
'ExchangeNotAvailable': {
|
||||
'OnMaintenance': {},
|
||||
},
|
||||
'InvalidNonce': {},
|
||||
'RequestTimeout': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = errorHierarchy;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const errorHierarchy = require ('./errorHierarchy.js')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
function subclass (BaseClass, classes, namespace = {}) {
|
||||
|
||||
for (const [className, subclasses] of Object.entries (classes)) {
|
||||
|
||||
const Class = Object.assign (namespace, {
|
||||
|
||||
/* By creating a named property, we trick compiler to assign our class constructor function a name.
|
||||
Otherwise, all our error constructors would be shown as [Function: Error] in the debugger! And
|
||||
the super-useful `e.constructor.name` magic wouldn't work — we then would have no chance to
|
||||
obtain a error type string from an error instance programmatically! */
|
||||
|
||||
[className]: class extends BaseClass {
|
||||
|
||||
constructor (message) {
|
||||
|
||||
super (message)
|
||||
|
||||
/* A workaround to make `instanceof` work on custom Error classes in transpiled ES5.
|
||||
See my blog post for the explanation of this hack:
|
||||
|
||||
https://medium.com/@xpl/javascript-deriving-from-error-properly-8d2f8f315801 */
|
||||
|
||||
this.constructor = Class
|
||||
this.__proto__ = Class.prototype
|
||||
this.name = className
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
})[className]
|
||||
|
||||
subclass (Class, subclasses, namespace)
|
||||
}
|
||||
|
||||
return namespace
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = subclass (
|
||||
// Root class
|
||||
Error,
|
||||
// Derived class hierarchy
|
||||
errorHierarchy
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { unCamelCase } = require ('./functions/string')
|
||||
|
||||
const unCamelCasePropertyNames = x => {
|
||||
for (const k in x) x[unCamelCase (k)] = x[k] // camel_case_method = camelCaseMethod
|
||||
return x
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = unCamelCasePropertyNames (Object.assign ({}
|
||||
|
||||
, require ('./functions/platform')
|
||||
, require ('./functions/generic')
|
||||
, require ('./functions/string')
|
||||
, require ('./functions/type')
|
||||
, require ('./functions/number')
|
||||
, require ('./functions/encode')
|
||||
, require ('./functions/crypto')
|
||||
, require ('./functions/time')
|
||||
, require ('./functions/throttle')
|
||||
, require ('./functions/misc')
|
||||
))
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
'use strict'
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
|
||||
const { capitalize } = require ('./string')
|
||||
const { stringToBase64, utf16ToBase64, urlencodeBase64 } = require ('./encode')
|
||||
const NodeRSA = require ('./../../static_dependencies/node-rsa/NodeRSA')
|
||||
const { numberToLE } = require ('./encode')
|
||||
const EC = require ('./../../static_dependencies/elliptic/lib/elliptic').ec
|
||||
const { ArgumentsRequired } = require ('./../errors')
|
||||
const BN = require ('../../static_dependencies/BN/bn.js')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const hash = (request, hash = 'md5', digest = 'hex') => {
|
||||
const options = {}
|
||||
if (hash === 'keccak') {
|
||||
hash = 'SHA3'
|
||||
options['outputLength'] = 256
|
||||
}
|
||||
const result = CryptoJS[hash.toUpperCase ()] (request, options)
|
||||
return (digest === 'binary') ? result : result.toString (CryptoJS.enc[capitalize (digest)])
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const hmac = (request, secret, hash = 'sha256', digest = 'hex') => {
|
||||
const result = CryptoJS['Hmac' + hash.toUpperCase ()] (request, secret)
|
||||
if (digest) {
|
||||
const encoding = (digest === 'binary') ? 'Latin1' : capitalize (digest)
|
||||
return result.toString (CryptoJS.enc[capitalize (encoding)])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
function rsa (request, secret, alg = 'RS256') {
|
||||
const algos = {
|
||||
'RS256': 'pkcs1-sha256',
|
||||
'RS512': 'pkcs1-sha512',
|
||||
}
|
||||
if (!(alg in algos)) {
|
||||
throw new ExchangeError (alg + ' is not a supported rsa signing algorithm.')
|
||||
}
|
||||
const algorithm = algos[alg]
|
||||
let key = new NodeRSA (secret, {
|
||||
'environment': 'browser',
|
||||
'signingScheme': algorithm,
|
||||
})
|
||||
return key.sign (request, 'base64', 'binary')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
function jwt (request, secret, alg = 'HS256') {
|
||||
const algos = {
|
||||
'HS256': 'sha256',
|
||||
'HS384': 'sha384',
|
||||
'HS512': 'sha512',
|
||||
};
|
||||
const encodedHeader = urlencodeBase64 (stringToBase64 (JSON.stringify ({ 'alg': alg, 'typ': 'JWT' })))
|
||||
const encodedData = urlencodeBase64 (stringToBase64 (JSON.stringify (request)))
|
||||
const token = [ encodedHeader, encodedData ].join ('.')
|
||||
const algoType = alg.slice (0, 2);
|
||||
const algorithm = algos[alg]
|
||||
let signature = undefined
|
||||
if (algoType === 'HS') {
|
||||
signature = urlencodeBase64 (hmac (token, secret, algorithm, 'base64'))
|
||||
} else if (algoType === 'RS') {
|
||||
signature = urlencodeBase64 (rsa (token, secret, alg))
|
||||
}
|
||||
return [ token, signature ].join ('.')
|
||||
}
|
||||
|
||||
function ecdsa (request, secret, algorithm = 'p256', hashFunction = undefined, fixedLength = false) {
|
||||
let digest = request
|
||||
if (hashFunction !== undefined) {
|
||||
digest = hash (request, hashFunction, 'hex')
|
||||
}
|
||||
const curve = new EC (algorithm)
|
||||
let signature = curve.sign (digest, secret, 'hex', { 'canonical': true })
|
||||
let counter = new BN ('0')
|
||||
const minimum_size = new BN ('1').shln (8 * 31).sub (new BN ('1'))
|
||||
while (fixedLength && (signature.r.gt (curve.nh) || signature.r.lte (minimum_size) || signature.s.lte (minimum_size))) {
|
||||
signature = curve.sign (digest, secret, 'hex', { 'canonical': true, 'extraEntropy': counter.toArray ('le', 32)})
|
||||
counter = counter.add (new BN ('1'))
|
||||
}
|
||||
return {
|
||||
'r': signature.r.toString (16).padStart (64, '0'),
|
||||
's': signature.s.toString (16).padStart (64, '0'),
|
||||
'v': signature.recoveryParam,
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const totp = (secret) => {
|
||||
|
||||
const dec2hex = s => ((s < 15.5 ? '0' : '') + Math.round (s).toString (16))
|
||||
, hex2dec = s => parseInt (s, 16)
|
||||
, leftpad = (s, p) => (p + s).slice (-p.length) // both s and p are short strings
|
||||
|
||||
const base32tohex = (base32) => {
|
||||
let base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
||||
let bits = ''
|
||||
let hex = ''
|
||||
for (let i = 0; i < base32.length; i++) {
|
||||
let val = base32chars.indexOf (base32.charAt (i).toUpperCase ())
|
||||
bits += leftpad (val.toString (2), '00000')
|
||||
}
|
||||
for (let i = 0; i + 4 <= bits.length; i += 4) {
|
||||
let chunk = bits.substr (i, 4)
|
||||
hex = hex + parseInt (chunk, 2).toString (16)
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
const getOTP = (secret) => {
|
||||
secret = secret.replace (' ', '') // support 2fa-secrets with spaces like "4TDV WOGO" → "4TDVWOGO"
|
||||
let epoch = Math.round (new Date ().getTime () / 1000.0)
|
||||
let time = leftpad (dec2hex (Math.floor (epoch / 30)), '0000000000000000')
|
||||
let hmacRes = hmac (CryptoJS.enc.Hex.parse (time), CryptoJS.enc.Hex.parse (base32tohex (secret)), 'sha1', 'hex')
|
||||
let offset = hex2dec (hmacRes.substring (hmacRes.length - 1))
|
||||
let otp = (hex2dec (hmacRes.substr (offset * 2, 8)) & hex2dec ('7fffffff')) + ''
|
||||
otp = (otp).substr (otp.length - 6, 6)
|
||||
return otp
|
||||
}
|
||||
|
||||
return getOTP (secret)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
hash,
|
||||
hmac,
|
||||
jwt,
|
||||
totp,
|
||||
rsa,
|
||||
ecdsa,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const CryptoJS = require ('../../static_dependencies/crypto-js/crypto-js')
|
||||
const qs = require ('../../static_dependencies/qs/index')
|
||||
const BN = require ('../../static_dependencies/BN/bn')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports =
|
||||
|
||||
{ json: (data, params = undefined) => JSON.stringify (data)
|
||||
, unjson: JSON.parse
|
||||
|
||||
, isJsonEncodedObject: object => (
|
||||
(typeof object === 'string') &&
|
||||
(object.length >= 2) &&
|
||||
((object[0] === '{') || (object[0] === '['))
|
||||
)
|
||||
|
||||
, stringToBinary (str) {
|
||||
const arr = new Uint8Array (str.length)
|
||||
for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt (i); }
|
||||
return CryptoJS.lib.WordArray.create (arr)
|
||||
}
|
||||
|
||||
, stringToBase64: string => CryptoJS.enc.Latin1.parse (string).toString (CryptoJS.enc.Base64)
|
||||
, utf16ToBase64: string => CryptoJS.enc.Utf16 .parse (string).toString (CryptoJS.enc.Base64)
|
||||
, base64ToBinary: string => CryptoJS.enc.Base64.parse (string)
|
||||
, base64ToString: string => CryptoJS.enc.Base64.parse (string).toString (CryptoJS.enc.Utf8)
|
||||
, binaryToBase64: binary => binary.toString (CryptoJS.enc.Base64)
|
||||
, base16ToBinary: string => CryptoJS.enc.Hex.parse (string)
|
||||
, binaryToBase16: binary => binary.toString (CryptoJS.enc.Hex)
|
||||
, binaryConcat: (...args) => args.reduce ((a, b) => a.concat (b))
|
||||
, binaryConcatArray: (arr) => arr.reduce ((a, b) => a.concat (b))
|
||||
|
||||
, urlencode: object => qs.stringify (object)
|
||||
, urlencodeWithArrayRepeat: object => qs.stringify (object, { arrayFormat: 'repeat' })
|
||||
, rawencode: object => qs.stringify (object, { encode: false })
|
||||
, encode: x => x
|
||||
, decode: x => x
|
||||
|
||||
// Url-safe-base64 without equals signs, with + replaced by - and slashes replaced by underscores
|
||||
|
||||
, urlencodeBase64: base64string => base64string.replace (/[=]+$/, '')
|
||||
.replace (/\+/g, '-')
|
||||
.replace (/\//g, '_')
|
||||
|
||||
, numberToLE: (n, padding) => {
|
||||
const hexArray = new BN (n).toArray ('le', padding)
|
||||
return byteArrayToWordArray (hexArray)
|
||||
}
|
||||
|
||||
, numberToBE: (n, padding) => {
|
||||
const hexArray = new BN (n).toArray ('be', padding)
|
||||
return byteArrayToWordArray (hexArray)
|
||||
}
|
||||
}
|
||||
|
||||
function byteArrayToWordArray(ba) {
|
||||
const wa = []
|
||||
for (let i = 0; i < ba.length; i++) {
|
||||
wa[(i / 4) | 0] |= ba[i] << (24 - 8 * i)
|
||||
}
|
||||
return CryptoJS.lib.WordArray.create (wa, ba.length)
|
||||
}
|
||||
|
||||
module.exports['byteArrayToWordArray'] = byteArrayToWordArray
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { isObject, isNumber, isDictionary, isArray } = require ('./type')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const keys = Object.keys
|
||||
|
||||
, values = x => !isArray (x) // don't copy arrays if they're already arrays!
|
||||
? Object.values (x)
|
||||
: x
|
||||
|
||||
, index = x => new Set (values (x))
|
||||
|
||||
, extend = (...args) => Object.assign ({}, ...args) // NB: side-effect free
|
||||
|
||||
, clone = x => isArray (x)
|
||||
? Array.from (x) // clones arrays
|
||||
: extend (x) // clones objects
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports =
|
||||
|
||||
{ keys
|
||||
, values
|
||||
, extend
|
||||
, clone
|
||||
, index
|
||||
, ordered: x => x // a stub to keep assoc keys in order (in JS it does nothing, it's mostly for Python)
|
||||
, unique: x => Array.from (index (x))
|
||||
, arrayConcat: (a, b) => a.concat (b)
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, inArray (needle, haystack) {
|
||||
|
||||
return haystack.includes (needle)
|
||||
}
|
||||
|
||||
, toArray (object) {
|
||||
|
||||
return Object.values (object)
|
||||
}
|
||||
|
||||
, isEmpty (object) {
|
||||
if (!object)
|
||||
return true;
|
||||
return (Array.isArray (object) ? object : Object.keys (object)).length < 1;
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, keysort (x, out = {}) {
|
||||
|
||||
for (const k of keys (x).sort ())
|
||||
out[k] = x[k]
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects and a key name to be used as an index:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
|
||||
Returns a map:
|
||||
{
|
||||
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
value2: { someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
value3: { someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
}
|
||||
*/
|
||||
|
||||
, indexBy (x, k, out = {}) {
|
||||
|
||||
for (const v of values (x))
|
||||
if (k in v)
|
||||
out[v[k]] = v
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects and a key name to be used as a grouping parameter:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
|
||||
Returns a map:
|
||||
{
|
||||
value1: [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue2' },
|
||||
]
|
||||
value3: [
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' }
|
||||
],
|
||||
}
|
||||
*/
|
||||
|
||||
, groupBy (x, k, out = {}) {
|
||||
|
||||
for (const v of values (x)) {
|
||||
if (k in v) {
|
||||
const p = v[k]
|
||||
out[p] = out[p] || []
|
||||
out[p].push (v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
/*
|
||||
Accepts a map/array of objects, a key name and a key value to be used as a filter:
|
||||
array = [
|
||||
{ someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
{ someKey: 'value2', anotherKey: 'anotherValue2' },
|
||||
{ someKey: 'value3', anotherKey: 'anotherValue3' },
|
||||
]
|
||||
key = 'someKey'
|
||||
value = 'value1'
|
||||
|
||||
Returns an array:
|
||||
[
|
||||
value1: { someKey: 'value1', anotherKey: 'anotherValue1' },
|
||||
]
|
||||
*/
|
||||
|
||||
, filterBy (x, k, value = undefined, out = []) {
|
||||
|
||||
for (const v of values (x))
|
||||
if (v[k] === value)
|
||||
out.push (v)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, sortBy: (array, // NB: MUTATES ARRAY!
|
||||
key,
|
||||
descending = false,
|
||||
direction = descending ? -1 : 1) => array.sort ((a, b) =>
|
||||
((a[key] < b[key]) ? -direction :
|
||||
((a[key] > b[key]) ? direction : 0)))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, flatten: function flatten (x, out = []) {
|
||||
|
||||
for (const v of x) {
|
||||
if (isArray (v)) flatten (v, out)
|
||||
else out.push (v)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, pluck: (x, k) => values (x)
|
||||
.filter (v => k in v)
|
||||
.map (v => v[k])
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, omit (x, ...args) {
|
||||
|
||||
if (!Array.isArray (x)) {
|
||||
|
||||
const out = clone (x)
|
||||
|
||||
for (const k of args) {
|
||||
|
||||
if (isArray (k)) { // omit (x, ['a', 'b'])
|
||||
|
||||
for (const kk of k) {
|
||||
delete out[kk]
|
||||
}
|
||||
}
|
||||
|
||||
else delete out[k] // omit (x, 'a', 'b')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, sum (...xs) {
|
||||
|
||||
const ns = xs.filter (isNumber) // leave only numbers
|
||||
|
||||
return (ns.length > 0)
|
||||
? ns.reduce ((a, b) => a + b, 0)
|
||||
: undefined
|
||||
}
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
, deepExtend: function deepExtend (...xs) {
|
||||
let out = undefined
|
||||
for (const x of xs) {
|
||||
if (isDictionary (x)) {
|
||||
if (!isObject (out)) {
|
||||
out = {}
|
||||
}
|
||||
for (const k in x) {
|
||||
out[k] = deepExtend (out[k], x[k])
|
||||
}
|
||||
} else {
|
||||
out = x
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
const { ROUND_UP, ROUND_DOWN } = require ('./number')
|
||||
const { NotSupported } = require ('./../errors')
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// converts timeframe to seconds
|
||||
const parseTimeframe = (timeframe) => {
|
||||
|
||||
const amount = timeframe.slice (0, -1)
|
||||
const unit = timeframe.slice (-1)
|
||||
let scale = undefined;
|
||||
|
||||
if (unit === 'y') {
|
||||
scale = 60 * 60 * 24 * 365
|
||||
} else if (unit === 'M') {
|
||||
scale = 60 * 60 * 24 * 30
|
||||
} else if (unit === 'w') {
|
||||
scale = 60 * 60 * 24 * 7
|
||||
} else if (unit === 'd') {
|
||||
scale = 60 * 60 * 24
|
||||
} else if (unit === 'h') {
|
||||
scale = 60 * 60
|
||||
} else if (unit === 'm') {
|
||||
scale = 60
|
||||
} else if (unit === 's') {
|
||||
scale = 1
|
||||
} else {
|
||||
throw new NotSupported ('timeframe unit ' + unit + ' is not supported')
|
||||
}
|
||||
|
||||
return amount * scale
|
||||
}
|
||||
|
||||
const roundTimeframe = (timeframe, timestamp, direction = ROUND_DOWN) => {
|
||||
const ms = parseTimeframe (timeframe) * 1000
|
||||
// Get offset based on timeframe in milliseconds
|
||||
const offset = timestamp % ms
|
||||
return timestamp - offset + ((direction === ROUND_UP) ? ms : 0);
|
||||
}
|
||||
|
||||
// given a sorted arrays of trades (recent last) and a timeframe builds an array of OHLCV candles
|
||||
const buildOHLCVC = (trades, timeframe = '1m', since = -Infinity, limit = Infinity) => {
|
||||
const ms = parseTimeframe (timeframe) * 1000;
|
||||
const ohlcvs = [];
|
||||
const [ timestamp, /* open */, high, low, close, volume, count ] = [ 0, 1, 2, 3, 4, 5, 6 ];
|
||||
const oldest = Math.min (trades.length - 1, limit);
|
||||
|
||||
for (let i = 0; i <= oldest; i++) {
|
||||
const trade = trades[i];
|
||||
if (trade.timestamp < since)
|
||||
continue;
|
||||
const openingTime = Math.floor (trade.timestamp / ms) * ms; // shift to the edge of m/h/d (but not M)
|
||||
const candle = ohlcvs.length - 1;
|
||||
|
||||
if (candle === -1 || openingTime >= ohlcvs[candle][timestamp] + ms) {
|
||||
// moved to a new timeframe -> create a new candle from opening trade
|
||||
ohlcvs.push ([
|
||||
openingTime, // timestamp
|
||||
trade.price, // O
|
||||
trade.price, // H
|
||||
trade.price, // L
|
||||
trade.price, // C
|
||||
trade.amount, // V
|
||||
1, // count
|
||||
]);
|
||||
} else {
|
||||
// still processing the same timeframe -> update opening trade
|
||||
ohlcvs[candle][high] = Math.max (ohlcvs[candle][high], trade.price);
|
||||
ohlcvs[candle][low] = Math.min (ohlcvs[candle][low], trade.price);
|
||||
ohlcvs[candle][close] = trade.price;
|
||||
ohlcvs[candle][volume] += trade.amount;
|
||||
ohlcvs[candle][count]++;
|
||||
} // if
|
||||
} // for
|
||||
return ohlcvs;
|
||||
}
|
||||
|
||||
const extractParams = (string) => {
|
||||
const re = /{([\w-]+)}/g
|
||||
const matches = []
|
||||
let match = re.exec (string)
|
||||
while (match) {
|
||||
matches.push (match[1])
|
||||
match = re.exec (string)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
const implodeParams = (string, params) => {
|
||||
if (!Array.isArray (params)) {
|
||||
const keys = Object.keys (params)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (!Array.isArray (params[key])) {
|
||||
string = string.replace ('{' + key + '}', params[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
|
||||
aggregate (bidasks) {
|
||||
|
||||
const result = {}
|
||||
|
||||
for (let i = 0; i < bidasks.length; i++) {
|
||||
const [ price, volume ] = bidasks[i];
|
||||
if (volume > 0) {
|
||||
result[price] = (result[price] || 0) + volume
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys (result).map (price => [parseFloat (price), parseFloat (result[price])])
|
||||
},
|
||||
|
||||
parseTimeframe,
|
||||
roundTimeframe,
|
||||
buildOHLCVC,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
|
||||
implodeParams,
|
||||
extractParams
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
'use strict'
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
|
||||
NB: initially, I used objects for options passing:
|
||||
|
||||
decimalToPrecision ('123.456', { digits: 2, round: true, afterPoint: true })
|
||||
|
||||
...but it turns out it's hard to port that across different languages and it is also
|
||||
probably has a performance penalty -- while it's a performance critical code! So
|
||||
I switched to using named constants instead, as it is actually more readable and
|
||||
succinct, and surely doesn't come with any inherent performance downside:
|
||||
|
||||
decimalToPrecision ('123.456', ROUND, 2, DECIMAL_PLACES) */
|
||||
|
||||
const ROUND = 0 // rounding mode
|
||||
, TRUNCATE = 1
|
||||
, ROUND_UP = 2
|
||||
, ROUND_DOWN = 3
|
||||
|
||||
const DECIMAL_PLACES = 0 // digits counting mode
|
||||
, SIGNIFICANT_DIGITS = 1
|
||||
, TICK_SIZE = 2
|
||||
|
||||
const NO_PADDING = 0 // zero-padding mode
|
||||
, PAD_WITH_ZERO = 1
|
||||
|
||||
const precisionConstants = {
|
||||
ROUND,
|
||||
TRUNCATE,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
DECIMAL_PLACES,
|
||||
SIGNIFICANT_DIGITS,
|
||||
TICK_SIZE,
|
||||
NO_PADDING,
|
||||
PAD_WITH_ZERO,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
// See https://stackoverflow.com/questions/1685680/how-to-avoid-scientific-notation-for-large-numbers-in-javascript for discussion
|
||||
|
||||
function numberToString (x) { // avoids scientific notation for too large and too small numbers
|
||||
|
||||
if (typeof x === 'string') return x
|
||||
|
||||
const s = x.toString ()
|
||||
if (Math.abs (x) < 1.0) {
|
||||
const e = parseInt (s.split ('e-')[1])
|
||||
const neg = (s[0] === '-')
|
||||
if (e) {
|
||||
x *= Math.pow (10, e - 1)
|
||||
x = (neg ? '-' : '') + '0.' + (new Array (e)).join ('0') + x.toString ().substring (neg ? 3 : 2)
|
||||
return x
|
||||
}
|
||||
} else {
|
||||
const parts = s.split ('e')
|
||||
if (parts[1]) {
|
||||
let e = parseInt (parts[1])
|
||||
const m = parts[0].split ('.')
|
||||
if (m[1]) {
|
||||
e -= m[1].length
|
||||
}
|
||||
return m[0] + m[1] + (new Array (e + 1)).join ('0')
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// expects non-scientific notation
|
||||
|
||||
const truncate_regExpCache = []
|
||||
, truncate_to_string = (num, precision = 0) => {
|
||||
num = numberToString (num)
|
||||
if (precision > 0) {
|
||||
const re = truncate_regExpCache[precision] || (truncate_regExpCache[precision] = new RegExp ("([-]*\\d+\\.\\d{" + precision + "})(\\d)"))
|
||||
const [ , result] = num.toString ().match (re) || [null, num]
|
||||
return result.toString ()
|
||||
}
|
||||
return parseInt (num).toString ()
|
||||
}
|
||||
, truncate = (num, precision = 0) => parseFloat (truncate_to_string (num, precision))
|
||||
|
||||
function precisionFromString (string) {
|
||||
const split = string.replace (/0+$/g, '').split ('.')
|
||||
return (split.length > 1) ? (split[1].length) : 0
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const decimalToPrecision = (x, roundingMode
|
||||
, numPrecisionDigits
|
||||
, countingMode = DECIMAL_PLACES
|
||||
, paddingMode = NO_PADDING) => {
|
||||
|
||||
if (numPrecisionDigits < 0) {
|
||||
if (countingMode === TICK_SIZE) {
|
||||
throw new Error (`TICK_SIZE cant be used with negative numPrecisionDigits`)
|
||||
}
|
||||
const toNearest = Math.pow (10, -numPrecisionDigits)
|
||||
if (roundingMode === ROUND) {
|
||||
return (toNearest * decimalToPrecision (x / toNearest, roundingMode, 0, countingMode, paddingMode)).toString ()
|
||||
}
|
||||
if (roundingMode === TRUNCATE) {
|
||||
return (x - (x % toNearest)).toString ()
|
||||
}
|
||||
}
|
||||
|
||||
/* handle tick size */
|
||||
if (countingMode === TICK_SIZE) {
|
||||
const precisionDigitsString = decimalToPrecision (numPrecisionDigits, ROUND, 100, DECIMAL_PLACES, NO_PADDING)
|
||||
const newNumPrecisionDigits = precisionFromString (precisionDigitsString)
|
||||
let missing = x % numPrecisionDigits
|
||||
// See: https://github.com/ccxt/ccxt/pull/6486
|
||||
missing = Number (decimalToPrecision (missing, ROUND, 8, DECIMAL_PLACES, NO_PADDING));
|
||||
const fpError = decimalToPrecision (missing / numPrecisionDigits, ROUND, Math.max (newNumPrecisionDigits, 8), DECIMAL_PLACES, NO_PADDING)
|
||||
if (precisionFromString (fpError) !== 0) {
|
||||
if (roundingMode === ROUND) {
|
||||
if (x > 0) {
|
||||
if (missing >= numPrecisionDigits / 2) {
|
||||
x = x - missing + numPrecisionDigits
|
||||
} else {
|
||||
x = x - missing
|
||||
}
|
||||
} else {
|
||||
if (missing >= numPrecisionDigits / 2) {
|
||||
x = Number (x) - missing
|
||||
} else {
|
||||
x = Number (x) - missing - numPrecisionDigits
|
||||
}
|
||||
}
|
||||
} else if (roundingMode === TRUNCATE) {
|
||||
x = x - missing
|
||||
}
|
||||
}
|
||||
return decimalToPrecision (x, ROUND, newNumPrecisionDigits, DECIMAL_PLACES, paddingMode);
|
||||
}
|
||||
|
||||
/* Convert to a string (if needed), skip leading minus sign (if any) */
|
||||
|
||||
const str = numberToString (x)
|
||||
, isNegative = str[0] === '-'
|
||||
, strStart = isNegative ? 1 : 0
|
||||
, strEnd = str.length
|
||||
|
||||
/* Find the dot position in the source buffer */
|
||||
|
||||
for (var strDot = 0; strDot < strEnd; strDot++)
|
||||
if (str[strDot] === '.')
|
||||
break
|
||||
|
||||
const hasDot = strDot < str.length
|
||||
|
||||
/* Char code constants */
|
||||
|
||||
const MINUS = 45
|
||||
, DOT = 46
|
||||
, ZERO = 48
|
||||
, ONE = (ZERO + 1)
|
||||
, FIVE = (ZERO + 5)
|
||||
, NINE = (ZERO + 9)
|
||||
|
||||
/* For -123.4567 the `chars` array will hold 01234567 (leading zero is reserved for rounding cases when 099 → 100) */
|
||||
|
||||
const chars = new Uint8Array ((strEnd - strStart) + (hasDot ? 0 : 1))
|
||||
chars[0] = ZERO
|
||||
|
||||
/* Validate & copy digits, determine certain locations in the resulting buffer */
|
||||
|
||||
let afterDot = chars.length
|
||||
, digitsStart = -1 // significant digits
|
||||
, digitsEnd = -1
|
||||
|
||||
for (var i = 1, j = strStart; j < strEnd; j++, i++) {
|
||||
|
||||
const c = str.charCodeAt (j)
|
||||
|
||||
if (c === DOT) {
|
||||
afterDot = i--
|
||||
|
||||
} else if ((c < ZERO) || (c > NINE)) {
|
||||
throw new Error (`${str}: invalid number (contains an illegal character '${str[i - 1]}')`)
|
||||
|
||||
} else {
|
||||
chars[i] = c
|
||||
if ((c !== ZERO) && (digitsStart < 0)) digitsStart = i
|
||||
}
|
||||
}
|
||||
|
||||
if (digitsStart < 0) digitsStart = 1
|
||||
|
||||
/* Determine the range to cut */
|
||||
|
||||
let precisionStart = (countingMode === DECIMAL_PLACES) ? afterDot // 0.(0)001234567
|
||||
: digitsStart // 0.00(1)234567
|
||||
, precisionEnd = precisionStart +
|
||||
numPrecisionDigits
|
||||
|
||||
/* Reset the last significant digit index, as it will change during the rounding/truncation. */
|
||||
|
||||
digitsEnd = -1
|
||||
|
||||
/* Perform rounding/truncation per digit, from digitsEnd to digitsStart, by using the following
|
||||
algorithm (rounding 999 → 1000, as an example):
|
||||
|
||||
step = i=3 i=2 i=1 i=0
|
||||
|
||||
chars = 0999 0999 0900 1000
|
||||
memo = ---0 --1- -1-- 0--- */
|
||||
|
||||
let allZeros = true;
|
||||
let signNeeded = isNegative;
|
||||
|
||||
for (let i = chars.length - 1, memo = 0; i >= 0; i--) {
|
||||
|
||||
let c = chars[i]
|
||||
|
||||
if (i !== 0) {
|
||||
c += memo
|
||||
|
||||
if (i >= (precisionStart + numPrecisionDigits)) {
|
||||
|
||||
const ceil = (roundingMode === ROUND) &&
|
||||
(c >= FIVE) &&
|
||||
!((c === FIVE) && memo) // prevents rounding of 1.45 to 2
|
||||
|
||||
c = ceil ? (NINE + 1) : ZERO
|
||||
}
|
||||
if (c > NINE) { c = ZERO; memo = 1; }
|
||||
else memo = 0
|
||||
|
||||
} else if (memo) c = ONE // leading extra digit (0900 → 1000)
|
||||
|
||||
chars[i] = c
|
||||
|
||||
if (c !== ZERO) {
|
||||
allZeros = false
|
||||
digitsStart = i
|
||||
digitsEnd = (digitsEnd < 0) ? (i + 1) : digitsEnd
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the precision range, as `digitsStart` may have changed... & the need for a negative sign if it is only 0 */
|
||||
|
||||
if (countingMode === SIGNIFICANT_DIGITS) {
|
||||
precisionStart = digitsStart
|
||||
precisionEnd = precisionStart + numPrecisionDigits
|
||||
}
|
||||
|
||||
if (allZeros) {
|
||||
signNeeded = false
|
||||
}
|
||||
|
||||
/* Determine the input character range */
|
||||
|
||||
const readStart = ((digitsStart >= afterDot) || allZeros) ? (afterDot - 1) : digitsStart // 0.000(1)234 ----> (0).0001234
|
||||
, readEnd = (digitsEnd < afterDot) ? (afterDot ) : digitsEnd // 12(3)000 ----> 123000( )
|
||||
|
||||
/* Compute various sub-ranges */
|
||||
|
||||
const nSign = (signNeeded ? 1 : 0) // (-)123.456
|
||||
, nBeforeDot = (nSign + (afterDot - readStart)) // (-123).456
|
||||
, nAfterDot = Math.max (readEnd - afterDot, 0) // -123.(456)
|
||||
, actualLength = (readEnd - readStart) // -(123.456)
|
||||
, desiredLength = (paddingMode === NO_PADDING)
|
||||
? (actualLength) // -(123.456)
|
||||
: (precisionEnd - readStart) // -(123.456 )
|
||||
|
||||
, pad = Math.max (desiredLength - actualLength, 0) // -123.456( )
|
||||
, padStart = (nBeforeDot + 1 + nAfterDot) // -123.456( )
|
||||
, padEnd = (padStart + pad) // -123.456 ( )
|
||||
, isInteger = (nAfterDot + pad) === 0 // -123
|
||||
|
||||
/* Fill the output buffer with characters */
|
||||
|
||||
const out = new Uint8Array (nBeforeDot + (isInteger ? 0 : 1) + nAfterDot + pad)
|
||||
// ------------------------------------------------------------------------------------------ // ---------------------
|
||||
if (signNeeded) out[0] = MINUS // - minus sign
|
||||
for (i = nSign, j = readStart; i < nBeforeDot; i++, j++) out[i] = chars[j] // 123 before dot
|
||||
if (!isInteger) out[nBeforeDot] = DOT // . dot
|
||||
for (i = nBeforeDot + 1, j = afterDot; i < padStart; i++, j++) out[i] = chars[j] // 456 after dot
|
||||
for (i = padStart; i < padEnd; i++) out[i] = ZERO // 000 padding
|
||||
|
||||
/* Build a string from the output buffer */
|
||||
|
||||
return String.fromCharCode (...out)
|
||||
}
|
||||
|
||||
// toWei / fromWei
|
||||
|
||||
function fromWei (amount, decimals = 18) {
|
||||
if (amount === undefined) {
|
||||
return amount
|
||||
}
|
||||
const exponential = Math.floor (amount).toExponential () // wei must be whole numbers
|
||||
const [ n, exponent ] = exponential.split ('e')
|
||||
const newExponent = parseInt (exponent) - decimals
|
||||
return parseFloat (n + 'e' + newExponent)
|
||||
}
|
||||
|
||||
function toWei (amount, decimals = 18) {
|
||||
if (amount === undefined) {
|
||||
return amount
|
||||
}
|
||||
const exponential = parseFloat (amount).toExponential ()
|
||||
const [ n, exponent ] = exponential.split ('e')
|
||||
const newExponent = parseInt (exponent) + decimals
|
||||
return numberToString (Math.floor (parseFloat (n + 'e' + newExponent))) // wei must be whole numbers
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
toWei,
|
||||
fromWei,
|
||||
numberToString,
|
||||
precisionFromString,
|
||||
decimalToPrecision,
|
||||
truncate_to_string,
|
||||
truncate,
|
||||
precisionConstants,
|
||||
ROUND,
|
||||
TRUNCATE,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
DECIMAL_PLACES,
|
||||
SIGNIFICANT_DIGITS,
|
||||
TICK_SIZE,
|
||||
NO_PADDING,
|
||||
PAD_WITH_ZERO,
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
const updateCachedOrders = function (openOrders, symbol) {
|
||||
}
|
||||
|
||||
const updateOrders = function (oldOrders, newOrders, closeOpenOrders = false) {
|
||||
|
||||
for (let i = 0; i < newOrders.length; i++) {
|
||||
let newOrder = newOrders[i];
|
||||
}
|
||||
|
||||
// update local cache with open orders
|
||||
for (let j = 0; j < openOrders.length; j++) {
|
||||
const id = openOrders[j]['id'];
|
||||
this.orders[id] = openOrders[j];
|
||||
}
|
||||
let openOrdersIndexedById = this.indexBy (openOrders, 'id');
|
||||
let cachedOrderIds = Object.keys (this.orders);
|
||||
let result = [];
|
||||
for (let k = 0; k < cachedOrderIds.length; k++) {
|
||||
// match each cached order to an order in the open orders array
|
||||
// possible reasons why a cached order may be missing in the open orders array:
|
||||
// - order was closed or canceled -> update cache
|
||||
// - symbol mismatch (e.g. cached BTC/USDT, fetched ETH/USDT) -> skip
|
||||
let id = cachedOrderIds[k];
|
||||
let order = this.orders[id];
|
||||
result.push (order);
|
||||
if (!(id in openOrdersIndexedById)) {
|
||||
// cached order is not in open orders array
|
||||
// if we fetched orders by symbol and it doesn't match the cached order -> won't update the cached order
|
||||
if (symbol !== undefined && symbol !== order['symbol'])
|
||||
continue;
|
||||
// order is cached but not present in the list of open orders -> mark the cached order as closed
|
||||
if (order['status'] === 'open') {
|
||||
order = this.extend (order, {
|
||||
'status': 'closed', // likewise it might have been canceled externally (unnoticed by "us")
|
||||
'cost': undefined,
|
||||
'filled': order['amount'],
|
||||
'remaining': 0.0,
|
||||
});
|
||||
if (order['cost'] === undefined) {
|
||||
if (order['filled'] !== undefined)
|
||||
order['cost'] = order['filled'] * order['price'];
|
||||
}
|
||||
this.orders[id] = order;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
console.log (this.id, orders, openOrders, closedOrders, canceledOrders)
|
||||
process.exit ()
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
updateOrders,
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// There's been a lot of messing with this code...
|
||||
// The problem is to satisfy the following requirements:
|
||||
// - properly detect isNode == true on server side and isNode == false in the browser (on client side)
|
||||
// - make sure create-react-app, react-starter-kit and other react frameworks work
|
||||
// - make sure it does not break the browserified version (when linked into a html from a cdn)
|
||||
// - make sure it does not break the webpacking and babel-transpiled scripts
|
||||
// - make sure it works in Electron
|
||||
// - make sure it works with Angular.js
|
||||
// - make sure it does not break other possible usage scenarios
|
||||
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
const isElectron = typeof process !== 'undefined' &&
|
||||
typeof process.versions !== 'undefined' &&
|
||||
typeof process.versions.electron !== 'undefined'
|
||||
|
||||
const isWebWorker = typeof WorkerGlobalScope !== 'undefined' && (self instanceof WorkerGlobalScope)
|
||||
|
||||
const isWindows = typeof process !== 'undefined' && process.platform === "win32"
|
||||
|
||||
const isNode = !(isBrowser || isWebWorker)
|
||||
|
||||
const defaultFetch = typeof (fetch) === "undefined" ? require ('../../static_dependencies/fetch-ponyfill/fetch-node') ().fetch : fetch
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
isBrowser,
|
||||
isElectron,
|
||||
isWebWorker,
|
||||
isNode,
|
||||
isWindows,
|
||||
defaultFetch,
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const uuid = a => a ? (a ^ Math.random () * 16 >> a / 4).toString (16)
|
||||
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace (/[018]/g, uuid)
|
||||
|
||||
module.exports =
|
||||
|
||||
{ uuid
|
||||
|
||||
// hasFetchOHLCV → has_fetch_ohlcv; parseHTTPResponse → parse_http_response
|
||||
, unCamelCase: s => s.match (/^[A-Z0-9_]+$/) ? s : (s.replace (/[a-z0-9][A-Z]/g, x => x[0] + '_' + x[1]).replace(/[A-Z0-9][A-Z0-9][a-z]/g, x => x[0] + '_' + x[1] + x[2]).toLowerCase ())
|
||||
|
||||
, capitalize: s => s.length
|
||||
? (s.charAt (0).toUpperCase () + s.slice (1))
|
||||
: s
|
||||
, strip: s => s.replace(/^\s+|\s+$/g, '')
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const { sleep
|
||||
, now } = require ('./time')
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
module.exports = {
|
||||
|
||||
throttle: function throttle (cfg) {
|
||||
|
||||
let lastTimestamp = now ()
|
||||
, numTokens = (cfg.numTokens !== undefined) ? cfg.numTokens : cfg.capacity
|
||||
, running = false
|
||||
, counter = 0
|
||||
|
||||
const queue = []
|
||||
|
||||
return Object.assign ((rateLimit, cost) => {
|
||||
|
||||
if (queue.length > cfg.maxCapacity)
|
||||
throw new Error ('Backlog is over max capacity of ' + cfg.maxCapacity)
|
||||
|
||||
return new Promise (async (resolve, reject) => {
|
||||
|
||||
try {
|
||||
queue.push ({ cost, resolve, reject })
|
||||
|
||||
if (!running) {
|
||||
running = true
|
||||
while (queue.length > 0) {
|
||||
const hasEnoughTokens = cfg.capacity ? (numTokens > 0) : (numTokens >= 0)
|
||||
if (hasEnoughTokens) {
|
||||
if (queue.length > 0) {
|
||||
let { cost, resolve, reject } = queue[0]
|
||||
cost = (cost || cfg.defaultCost)
|
||||
if (numTokens >= Math.min (cost, cfg.capacity)) {
|
||||
numTokens -= cost
|
||||
queue.shift ()
|
||||
resolve ()
|
||||
}
|
||||
}
|
||||
}
|
||||
const t = now ()
|
||||
, elapsed = t - lastTimestamp
|
||||
lastTimestamp = t
|
||||
numTokens = Math.min (cfg.capacity, numTokens + elapsed * (1 / rateLimit))
|
||||
await sleep (cfg.delay)
|
||||
}
|
||||
running = false
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
reject (e)
|
||||
}
|
||||
})
|
||||
|
||||
}, cfg, { configure: newCfg => throttle (Object.assign ({}, cfg, newCfg)) })
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const now = Date.now // TODO: figure out how to utilize performance.now () properly – it's not as easy as it does not return a unix timestamp...
|
||||
const microseconds = () => now () * 1000 // TODO: utilize performance.now for that purpose
|
||||
const milliseconds = now
|
||||
const seconds = () => Math.floor (now () / 1000)
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const setTimeout_original = setTimeout
|
||||
const setTimeout_safe = (done, ms, setTimeout = setTimeout_original /* overrideable for mocking purposes */, targetTime = now () + ms) => {
|
||||
|
||||
/* The built-in setTimeout function can fire its callback earlier than specified, so we
|
||||
need to ensure that it does not happen: sleep recursively until `targetTime` is reached... */
|
||||
|
||||
let clearInnerTimeout = () => {}
|
||||
let active = true
|
||||
|
||||
const id = setTimeout (() => {
|
||||
active = true
|
||||
const rest = targetTime - now ()
|
||||
if (rest > 0) {
|
||||
clearInnerTimeout = setTimeout_safe (done, rest, setTimeout, targetTime) // try sleep more
|
||||
} else {
|
||||
done ()
|
||||
}
|
||||
}, ms)
|
||||
|
||||
return function clear () {
|
||||
if (active) {
|
||||
active = false // dunno if IDs are unique on various platforms, so it's better to rely on this flag to exclude the possible cancellation of the wrong timer (if called after completion)
|
||||
clearTimeout (id)
|
||||
}
|
||||
clearInnerTimeout ()
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
class TimedOut extends Error {
|
||||
|
||||
constructor () {
|
||||
const message = 'timed out'
|
||||
super (message)
|
||||
this.constructor = TimedOut
|
||||
this.__proto__ = TimedOut.prototype
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const iso8601 = (timestamp) => {
|
||||
let _timestampNumber = undefined;
|
||||
if (typeof timestamp === 'number') {
|
||||
_timestampNumber = Math.floor (timestamp);
|
||||
} else {
|
||||
_timestampNumber = parseInt (timestamp, 10);
|
||||
}
|
||||
|
||||
// undefined, null and lots of nasty non-numeric values yield NaN
|
||||
if (Number.isNaN (_timestampNumber) || _timestampNumber < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// last line of defence
|
||||
try {
|
||||
return new Date (_timestampNumber).toISOString ();
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const parse8601 = (x) => {
|
||||
if (typeof x !== 'string' || !x) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.match (/^[0-9]+$/)) {
|
||||
// a valid number in a string, not a date.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.indexOf ('-') < 0 || x.indexOf (':') < 0) { // no date can be without a dash and a colon
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// last line of defence
|
||||
try {
|
||||
const candidate = Date.parse (((x.indexOf ('+') >= 0) || (x.slice (-1) === 'Z')) ? x : (x + 'Z').replace (/\s(\d\d):/, 'T$1:'));
|
||||
if (Number.isNaN (candidate)) {
|
||||
return undefined;
|
||||
}
|
||||
return candidate;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const parseDate = (x) => {
|
||||
if (typeof x !== 'string' || !x) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.indexOf ('GMT') >= 0) {
|
||||
try {
|
||||
return Date.parse (x);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return parse8601 (x);
|
||||
}
|
||||
|
||||
const mdy = (timestamp, infix = '-') => {
|
||||
infix = infix || ''
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ().toString ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
m = m < 10 ? ('0' + m) : m.toString ()
|
||||
d = d < 10 ? ('0' + d) : d.toString ()
|
||||
return m + infix + d + infix + Y
|
||||
}
|
||||
|
||||
const ymd = (timestamp, infix = '-') => {
|
||||
infix = infix || ''
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ().toString ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
m = m < 10 ? ('0' + m) : m.toString ()
|
||||
d = d < 10 ? ('0' + d) : d.toString ()
|
||||
return Y + infix + m + infix + d
|
||||
}
|
||||
|
||||
const ymdhms = (timestamp, infix = ' ') => {
|
||||
const date = new Date (timestamp)
|
||||
const Y = date.getUTCFullYear ()
|
||||
let m = date.getUTCMonth () + 1
|
||||
let d = date.getUTCDate ()
|
||||
let H = date.getUTCHours ()
|
||||
let M = date.getUTCMinutes ()
|
||||
let S = date.getUTCSeconds ()
|
||||
m = m < 10 ? ('0' + m) : m
|
||||
d = d < 10 ? ('0' + d) : d
|
||||
H = H < 10 ? ('0' + H) : H
|
||||
M = M < 10 ? ('0' + M) : M
|
||||
S = S < 10 ? ('0' + S) : S
|
||||
return Y + '-' + m + '-' + d + infix + H + ':' + M + ':' + S
|
||||
}
|
||||
|
||||
module.exports =
|
||||
|
||||
{
|
||||
now
|
||||
, microseconds
|
||||
, milliseconds
|
||||
, seconds
|
||||
, iso8601
|
||||
, parse8601
|
||||
, parseDate
|
||||
, mdy
|
||||
, ymd
|
||||
, ymdhms
|
||||
, setTimeout_safe
|
||||
, sleep: ms => new Promise (resolve => setTimeout_safe (resolve, ms))
|
||||
, TimedOut
|
||||
, timeout: async (ms, promise) => {
|
||||
|
||||
let clear = () => {}
|
||||
const expires = new Promise (resolve => (clear = setTimeout_safe (resolve, ms)))
|
||||
|
||||
try {
|
||||
return await Promise.race ([promise, expires.then (() => { throw new TimedOut () })])
|
||||
} finally {
|
||||
clear () // fixes https://github.com/ccxt/ccxt/issues/749
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
const isNumber = Number.isFinite
|
||||
, isInteger = Number.isInteger
|
||||
, isArray = Array.isArray
|
||||
, hasProps = o => ((o !== undefined) && (o !== null))
|
||||
, isString = s => (typeof s === 'string')
|
||||
, isObject = o => ((o !== null) && (typeof o === 'object'))
|
||||
, isDictionary = o => (isObject (o) && !isArray (o))
|
||||
, isStringCoercible = x => ((hasProps (x) && x.toString) || isNumber (x))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const prop = (o, k) => (isObject (o) ? o[k] : undefined)
|
||||
, prop2 = (o, k1, k2) => (!isObject (o) ? undefined : (((k1 in o) && (o[k1] !== null)) ? o[k1] : o[k2]))
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
const asFloat = x => ((isNumber (x) || isString (x)) ? parseFloat (x) : NaN)
|
||||
, asInteger = x => ((isNumber (x) || isString (x)) ? parseInt (x, 10) : NaN)
|
||||
|
||||
/* ............................................. */
|
||||
|
||||
module.exports = {
|
||||
|
||||
isNumber
|
||||
, isInteger
|
||||
, isArray
|
||||
, isObject
|
||||
, isString
|
||||
, isStringCoercible
|
||||
, isDictionary
|
||||
|
||||
, hasProps
|
||||
, prop
|
||||
|
||||
, asFloat
|
||||
, asInteger
|
||||
|
||||
, safeFloat: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? n : $default)
|
||||
, safeInteger: (o, k, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? n : $default)
|
||||
, safeIntegerProduct: (o, k, $factor, $default, n = asInteger (prop (o, k))) => (isNumber (n) ? parseInt (n * $factor) : $default)
|
||||
, safeTimestamp: (o, k, $default, n = asFloat (prop (o, k))) => (isNumber (n) ? parseInt (n * 1000) : $default)
|
||||
, safeValue: (o, k, $default, x = prop (o, k)) => (hasProps (x) ? x : $default)
|
||||
, safeString: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x) : $default)
|
||||
, safeStringLower: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
|
||||
, safeStringUpper: (o, k, $default, x = prop (o, k)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
|
||||
|
||||
// not using safeFloats with an array argument as we're trying to save some cycles here
|
||||
// we're not using safeFloat3 either because those cases are too rare to deserve their own optimization
|
||||
|
||||
, safeFloat2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
|
||||
, safeInteger2: (o, k1, k2, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? n : $default)
|
||||
, safeIntegerProduct2: (o, k1, k2, $factor, $default, n = asInteger (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * $factor) : $default)
|
||||
, safeTimestamp2: (o, k1, k2, $default, n = asFloat (prop2 (o, k1, k2))) => (isNumber (n) ? parseInt (n * 1000) : $default)
|
||||
, safeValue2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (hasProps (x) ? x : $default)
|
||||
, safeString2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x) : $default)
|
||||
, safeStringLower2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
|
||||
, safeStringUpper2: (o, k1, k2, $default, x = prop2 (o, k1, k2)) => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
|
||||
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
Reference in new issue
Block a user