diff --git a/lamassu-admin-elm/src/NavBar.elm b/lamassu-admin-elm/src/NavBar.elm index d0cfb091..ba2f5792 100644 --- a/lamassu-admin-elm/src/NavBar.elm +++ b/lamassu-admin-elm/src/NavBar.elm @@ -258,6 +258,7 @@ view route invalidGroups = , ( "Bitstamp", AccountRoute "bitstamp", True ) , ( "Blockcypher", AccountRoute "blockcypher", True ) , ( "Infura", AccountRoute "infura", True ) + , ( "itBit", AccountRoute "itbit", True ) , ( "Kraken", AccountRoute "kraken", True ) , ( "Mailgun", AccountRoute "mailgun", True ) , ( "QuadrigaCX", AccountRoute "quadrigacx", True ) diff --git a/lib/admin/config.js b/lib/admin/config.js index fd1e9a7c..825f08a0 100644 --- a/lib/admin/config.js +++ b/lib/admin/config.js @@ -179,6 +179,7 @@ function fetchData () { {code: 'kraken', display: 'Kraken', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']}, {code: 'bitstamp', display: 'Bitstamp', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, {code: 'coinbase', display: 'Coinbase', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, + {code: 'itbit', display: 'itBit', class: 'ticker', cryptos: ['BTC']}, {code: 'quadrigacx', display: 'QuadrigaCX', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, {code: 'mock-ticker', display: 'Mock ticker', class: 'ticker', cryptos: ALL_CRYPTOS}, {code: 'bitcoind', display: 'bitcoind', class: 'wallet', cryptos: ['BTC']}, @@ -191,6 +192,7 @@ function fetchData () { {code: 'bitcoincashd', display: 'bitcoincashd', class: 'wallet', cryptos: ['BCH']}, {code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']}, {code: 'bitstamp', display: 'Bitstamp', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, + {code: 'itbit', display: 'itBit', class: 'exchange', cryptos: ['BTC']}, {code: 'kraken', display: 'Kraken', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']}, {code: 'quadrigacx', display: 'QuadrigaCX', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, {code: 'mock-wallet', display: 'Mock (Caution!)', class: 'wallet', cryptos: ALL_CRYPTOS}, diff --git a/lib/plugins/common/itbit.js b/lib/plugins/common/itbit.js new file mode 100644 index 00000000..e169c05c --- /dev/null +++ b/lib/plugins/common/itbit.js @@ -0,0 +1,92 @@ +'use strict' + +const querystring = require('querystring') +const axios = require('axios') +const crypto = require('crypto') +const _ = require('lodash/fp') + +const API_ENDPOINT = 'https://api.itbit.com/v1' + +let counter = -1 +let lastTimestamp = Date.now() + +function generateNonce () { + const timestamp = Date.now() + if (timestamp !== lastTimestamp) counter = -1 + lastTimestamp = timestamp + counter = (counter + 1) % 1000 + return timestamp.toString() + counter.toString() +} + +function authRequest (account, method, path, data) { + if (!account.userId || !account.walletId || !account.clientKey || !account.clientSecret) { + const err = new Error('Must provide user ID, wallet ID, client key, and client secret') + return Promise.reject(err) + } + + const url = buildURL(method, path, data) + const dataString = method !== 'GET' && !_.isEmpty(data) ? JSON.stringify(data) : '' + const nonce = generateNonce() + const timestamp = Date.now() + const message = nonce + JSON.stringify([method, url, dataString, nonce.toString(), timestamp.toString()]) + + const hashBuffer = crypto + .createHash('sha256') + .update(message).digest() + + const bufferToHash = Buffer.concat([Buffer.from(url), hashBuffer]) + + const signature = crypto + .createHmac('sha512', Buffer.from(account.clientSecret)) + .update(bufferToHash) + .digest('base64') + + return request(method, path, data, { + 'Authorization': account.clientKey + ':' + signature, + 'X-Auth-Timestamp': timestamp, + 'X-Auth-Nonce': nonce + }) +} + +function request (method, path, data, auth) { + const options = { + method: method, + url: buildURL(method, path, data), + headers: { + 'User-Agent': 'Lamassu itBit node.js client', + ...(auth) + }, + ...(method !== 'GET' && { data: data }) + } + + return axios(options) + .then(r => r.data) + .catch(e => { + var description = _.get(e, 'response.data.description') + throw new Error(description || e.message) + }) +} + +const cryptoCodeTranslations = { 'BTC': 'XBT', 'ETH': 'ETH' } +function buildMarket (fiatCode, cryptoCode) { + const translatedCryptoCode = cryptoCodeTranslations[cryptoCode] + if (!translatedCryptoCode) { + throw new Error('Unsupported crypto: ' + cryptoCode) + } + + if (!_.includes(fiatCode, ['USD', 'EUR', 'SGD'])) { + throw new Error('Unsupported fiat: ' + fiatCode) + } + + return translatedCryptoCode + fiatCode +} + +function buildURL (method, path, data) { + let url = API_ENDPOINT + path + if (method === 'GET' && !_.isEmpty(data)) { + url += '?' + querystring.stringify(data) + } + return url +} + +module.exports = { authRequest, request, buildMarket } diff --git a/lib/plugins/exchange/itbit/itbit.js b/lib/plugins/exchange/itbit/itbit.js new file mode 100644 index 00000000..05d6d7fa --- /dev/null +++ b/lib/plugins/exchange/itbit/itbit.js @@ -0,0 +1,45 @@ +const common = require('../../common/itbit') +const coinUtils = require('../../../coin-utils') + +exports.buy = function (account, cryptoAtoms, fiatCode, cryptoCode) { + return trade('buy', account, cryptoAtoms, fiatCode, cryptoCode) +} + +exports.sell = function (account, cryptoAtoms, fiatCode, cryptoCode) { + return trade('sell', account, cryptoAtoms, fiatCode, cryptoCode) +} + +function trade (type, account, cryptoAtoms, fiatCode, cryptoCode) { + try { + const instrument = common.buildMarket(fiatCode, cryptoCode) + const cryptoAmount = coinUtils.toUnit(cryptoAtoms, cryptoCode) + + return calculatePrice(type, instrument, cryptoAmount) + .then(price => { + const args = { + side: type, + type: 'limit', + currency: cryptoCode, + amount: cryptoAmount.toFixed(4), + price: price.toFixed(2), + instrument: instrument + } + return common.authRequest(account, 'POST', '/wallets/' + account.walletId + '/orders', args) + }) + } catch (e) { + return Promise.reject(e) + } +} + +function calculatePrice (type, tickerSymbol, amount) { + return common.request('GET', '/markets/' + tickerSymbol + '/order_book') + .then(orderBook => { + const book = type == 'buy' ? 'asks' : 'bids' + let collected = 0.0 + for (const entry of orderBook[book]) { + collected += parseFloat(entry[1]) + if (collected >= amount) return parseFloat(entry[0]) + } + throw new Error('Insufficient market depth') + }) +} diff --git a/lib/plugins/ticker/itbit/itbit.js b/lib/plugins/ticker/itbit/itbit.js new file mode 100644 index 00000000..2b98e8a4 --- /dev/null +++ b/lib/plugins/ticker/itbit/itbit.js @@ -0,0 +1,54 @@ +const axios = require('axios') +const _ = require('lodash/fp') + +const BN = require('../../../bn') +const common = require('../../common/itbit') + +exports.NAME = 'itBit' +exports.SUPPORTED_MODULES = ['ticker'] + +function findCurrency (fxRates, fiatCode) { + const rates = _.find(_.matchesProperty('code', fiatCode), fxRates) + if (!rates || !rates.rate) throw new Error(`Unsupported currency: ${fiatCode}`) + return BN(rates.rate) +} + +exports.ticker = function ticker (account, fiatCode, cryptoCode) { + if (_.includes(fiatCode, ['USD', 'EUR', 'SGD'])) { + return getCurrencyRates(fiatCode, cryptoCode) + } + + return axios.get('https://bitpay.com/api/rates') + .then(response => { + const fxRates = response.data + try { + const usdRate = findCurrency(fxRates, 'USD') + const fxRate = findCurrency(fxRates, fiatCode).div(usdRate) + + return getCurrencyRates('USD', cryptoCode) + .then(res => ({ + rates: { + ask: res.rates.ask.times(fxRate), + bid: res.rates.bid.times(fxRate) + } + })) + } catch (e) { + return Promise.reject(e) + } + }) +} + +function getCurrencyRates (fiatCode, cryptoCode) { + try { + const market = common.buildMarket(fiatCode, cryptoCode) + return common.request('GET', '/markets/' + market + '/ticker') + .then(r => ({ + rates: { + ask: BN(r.ask), + bid: BN(r.bid) + } + })) + } catch (e) { + return Promise.reject(e) + } +} diff --git a/package-lock.json b/package-lock.json index 1e04c7ba..19d12c21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9017,6 +9017,356 @@ "signal-exit": "^3.0.2" } }, + "rewire": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-4.0.1.tgz", + "integrity": "sha512-+7RQ/BYwTieHVXetpKhT11UbfF6v1kGhKFrtZN7UDL2PybMsSt/rpLWeEUGF5Ndsl1D5BxiCB14VDJyoX+noYw==", + "dev": true, + "requires": { + "eslint": "^4.19.1" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint": { + "version": "4.19.1", + "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.4", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^1.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + } + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "lru-cache": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.4.tgz", + "integrity": "sha512-EPstzZ23znHUVLKj+lcXO1KvZkrlw+ZirdwvOmnAnA/1PB4ggyXJ77LRkCqkff+ShQ+cqoxCxLQOh4cKITO5iA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^3.0.2" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", + "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", + "dev": true + }, + "regexpp": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + } + } + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -9176,6 +9526,21 @@ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", "dev": true }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, "rxjs": { "version": "5.5.10", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", diff --git a/package.json b/package.json index d197a9ed..2ec9ed0c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "devDependencies": { "ava": "^0.19.1", "mocha": "^5.0.1", + "rewire": "^4.0.1", "standard": "^12.0.1" }, "standard": { diff --git a/public/elm.js b/public/elm.js index 05b5da60..d044c4b4 100644 --- a/public/elm.js +++ b/public/elm.js @@ -37732,43 +37732,52 @@ var _user$project$NavBar$view = F2( ctor: '::', _0: { ctor: '_Tuple3', - _0: 'Kraken', - _1: _user$project$CoreTypes$AccountRoute('kraken'), + _0: 'itBit', + _1: _user$project$CoreTypes$AccountRoute('itbit'), _2: true }, _1: { ctor: '::', _0: { ctor: '_Tuple3', - _0: 'Mailgun', - _1: _user$project$CoreTypes$AccountRoute('mailgun'), + _0: 'Kraken', + _1: _user$project$CoreTypes$AccountRoute('kraken'), _2: true }, _1: { ctor: '::', _0: { ctor: '_Tuple3', - _0: 'QuadrigaCX', - _1: _user$project$CoreTypes$AccountRoute('quadrigacx'), + _0: 'Mailgun', + _1: _user$project$CoreTypes$AccountRoute('mailgun'), _2: true }, _1: { ctor: '::', _0: { ctor: '_Tuple3', - _0: 'Strike', - _1: _user$project$CoreTypes$AccountRoute('strike'), + _0: 'QuadrigaCX', + _1: _user$project$CoreTypes$AccountRoute('quadrigacx'), _2: true }, _1: { ctor: '::', _0: { ctor: '_Tuple3', - _0: 'Twilio', - _1: _user$project$CoreTypes$AccountRoute('twilio'), + _0: 'Strike', + _1: _user$project$CoreTypes$AccountRoute('strike'), _2: true }, - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: { + ctor: '_Tuple3', + _0: 'Twilio', + _1: _user$project$CoreTypes$AccountRoute('twilio'), + _2: true + }, + _1: {ctor: '[]'} + } } } } diff --git a/schemas/itbit.json b/schemas/itbit.json new file mode 100644 index 00000000..0713b741 --- /dev/null +++ b/schemas/itbit.json @@ -0,0 +1,34 @@ +{ + "code": "itbit", + "display": "itBit", + "fields": [ + { + "code": "userId", + "display": "User ID", + "fieldType": "string", + "required": true, + "value": "" + }, + { + "code": "walletId", + "display": "Wallet ID", + "fieldType": "string", + "required": true, + "value": "" + }, + { + "code": "clientKey", + "display": "Client key", + "fieldType": "string", + "required": true, + "value": "" + }, + { + "code": "clientSecret", + "display": "Client secret", + "fieldType": "password", + "required": true, + "value": "" + } + ] +} diff --git a/test/unit/itbit-calculate-price.js b/test/unit/itbit-calculate-price.js new file mode 100644 index 00000000..884a46dc --- /dev/null +++ b/test/unit/itbit-calculate-price.js @@ -0,0 +1,78 @@ +import test from 'ava' +import rewire from 'rewire' + +function rewireCalculatePrice (commonMock) { + const itbit = rewire('../../lib/plugins/exchange/itbit/itbit') + + itbit.__set__('common', commonMock) + + const calculatePrice = itbit.__get__('calculatePrice') + + return calculatePrice +} + +test('calculate minimum available price for buy', async t => { + const commonMock = { + request () { + return Promise.resolve({ + asks: [ + [2, 10], + [4, 15], + [4.5, 17] + ], + bids: [] + }) + } + } + + const calculatePrice = rewireCalculatePrice(commonMock) + + let price = await calculatePrice('buy', 'XBTUSD', 20) + + t.is(price, 4) +}) + +test('calculate minimum available price for sell', async t => { + const commonMock = { + request () { + return Promise.resolve({ + bids: [ + [2, 10], + [3, 15], + [4.5, 17] + ], + asks: [] + }) + } + } + + const calculatePrice = rewireCalculatePrice(commonMock) + + let price = await calculatePrice('sell', 'XBTUSD', 20) + + t.is(price, 3) +}) + +test('throw error on insufficient trade depth', async t => { + t.plan(1) + + const commonMock = { + request () { + return Promise.resolve({ + asks: [ + [2, 10], + [4, 15], + [4.5, 17] + ], + bids: [] + }) + } + } + + const calculatePrice = rewireCalculatePrice(commonMock) + + calculatePrice('buy', 'XBTUSD', 100) + .catch(err => { + t.is(err.message, 'Insufficient market depth') + }) +}) diff --git a/test/unit/itbit-get-currency-rates.js b/test/unit/itbit-get-currency-rates.js new file mode 100644 index 00000000..3dea9aa1 --- /dev/null +++ b/test/unit/itbit-get-currency-rates.js @@ -0,0 +1,48 @@ +import test from 'ava' +import rewire from 'rewire' +import BN from '../../lib/bn' + +function rewireGetCurrencyRates (commonMock) { + const itbit = rewire('../../lib/plugins/ticker/itbit/itbit') + + itbit.__set__('common', commonMock) + + const getCurrencyRates = itbit.__get__('getCurrencyRates') + + return getCurrencyRates +} + +test('get currency rates of BTC USD', async t => { + function mockRequest() { + return Promise.resolve({ + pair: 'XBTUSD', + bid: '622', + bidAmt: '0.0006', + ask: '641.29', + askAmt: '0.5', + lastPrice: '618.00000000', + lastAmt: '0.00040000', + volume24h: '0.00040000', + volumeToday: '0.00040000', + high24h: '618.00000000', + low24h: '618.00000000', + highToday: '618.00000000', + lowToday: '618.00000000', + openToday: '618.00000000', + vwapToday: '618.00000000', + vwap24h: '618.00000000', + serverTimeUTC: '2014-06-24T20:42:35.6160000Z' + }) + } + + const common = rewire('../../lib/plugins/common/itbit') + + common.request = mockRequest + + const getCurrencyRates = rewireGetCurrencyRates(common) + + let result = await getCurrencyRates('USD', 'BTC') + + t.true(result.rates.bid.eq('622')) + t.true(result.rates.ask.eq('641.29')) +}) diff --git a/test/unit/itbit-trade.js b/test/unit/itbit-trade.js new file mode 100644 index 00000000..7b51bc13 --- /dev/null +++ b/test/unit/itbit-trade.js @@ -0,0 +1,35 @@ +import test from 'ava' +import rewire from 'rewire' +import BN from '../../lib/bn' + +function rewireTrade (commonMock, calculatePrice = () => Promise.resolve(15)) { + const itbit = rewire('../../lib/plugins/exchange/itbit/itbit') + + itbit.__set__('common', commonMock) + itbit.__set__('calculatePrice', calculatePrice) + + const trade = itbit.__get__('trade') + + return trade +} + +test('should handle itbit error response', async t => { + t.plan(1) + + const commonMock = { + mock: true, + authRequest () { + return Promise.reject(new Error('The wallet provided does not have the funds required to place the order.')) + }, + buildMarket () { + return 'XBTUSD' + } + } + + const trade = rewireTrade(commonMock) + + trade('buy', { walletId: 'id' }, BN('93410'), 'USD', 'BTC') + .catch(err => { + t.regex(err.message, /wallet provided/g) + }) +})