From bcf336741e9a990c21e68042d63724866f0cabe4 Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Thu, 27 Sep 2018 15:59:53 -0300 Subject: [PATCH] Add QuadrigaCX exchange and ticker (#176) * Add QuadrigaCX exchange and ticker * Clean up code from QuadrigaCX --- .vscode/launch.json | 6 ++ lib/admin/config.js | 4 +- lib/plugins/common/quadrigacx.js | 91 +++++++++++++++++++ lib/plugins/exchange/quadrigacx/quadrigacx.js | 28 ++++++ lib/plugins/ticker/quadrigacx/quadrigacx.js | 49 ++++++++++ public/elm.js | 45 ++++++--- schemas/quadrigacx.json | 27 ++++++ 7 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 lib/plugins/common/quadrigacx.js create mode 100644 lib/plugins/exchange/quadrigacx/quadrigacx.js create mode 100644 lib/plugins/ticker/quadrigacx/quadrigacx.js create mode 100644 schemas/quadrigacx.json diff --git a/.vscode/launch.json b/.vscode/launch.json index f167992a..fd6e3809 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,12 @@ "program": "${workspaceRoot}/bin/lamassu-server", "cwd": "${workspaceRoot}", "args": ["--mockSms"] + }, + { + "type": "node", + "request": "attach", + "name": "Attach by Process ID", + "processId": "${command:PickProcess}" } ] } diff --git a/lib/admin/config.js b/lib/admin/config.js index 8e2c570d..b8b3882e 100644 --- a/lib/admin/config.js +++ b/lib/admin/config.js @@ -172,6 +172,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: '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']}, {code: 'no-layer2', display: 'No Layer 2', class: 'layer2', cryptos: ALL_CRYPTOS}, @@ -184,6 +185,7 @@ function fetchData () { {code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']}, {code: 'bitstamp', display: 'Bitstamp', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, {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}, {code: 'no-exchange', display: 'No exchange', class: 'exchange', cryptos: ALL_CRYPTOS}, {code: 'mock-exchange', display: 'Mock exchange', class: 'exchange', cryptos: ALL_CRYPTOS}, @@ -194,7 +196,7 @@ function fetchData () { {code: 'all-zero-conf', display: 'Always 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']}, {code: 'no-zero-conf', display: 'Always 1-conf', class: 'zeroConf', cryptos: ALL_CRYPTOS}, {code: 'blockcypher', display: 'Blockcypher', class: 'zeroConf', cryptos: ['BTC']}, - {code: 'mock-zero-conf', display: 'Mock 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']} + {code: 'mock-zero-conf', display: 'Mock 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH', 'ETH']} ], machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name})) })) diff --git a/lib/plugins/common/quadrigacx.js b/lib/plugins/common/quadrigacx.js new file mode 100644 index 00000000..81c5527a --- /dev/null +++ b/lib/plugins/common/quadrigacx.js @@ -0,0 +1,91 @@ +const axios = require('axios') +const crypto = require('crypto') +const _ = require('lodash/fp') + +const API_ENDPOINT = 'https://api.quadrigacx.com/v2' + +let counter = -1 +let lastTimestamp = Date.now() + +function pad (num) { + const asString = num.toString(10) + if (num < 10) return '00' + asString + if (num < 100) return '0' + asString + return asString +} + +function generateNonce () { + const timestamp = Date.now() + if (timestamp !== lastTimestamp) counter = -1 + lastTimestamp = timestamp + counter = (counter + 1) % 1000 + return timestamp.toString(10) + pad(counter) +} + +function authRequest (config, path, data) { + if (!config.key || !config.secret || !config.clientId) { + const err = new Error('Must provide key, secret and client ID') + return Promise.reject(err) + } + + data = data || {} + + const nonce = generateNonce() + const msg = [nonce, config.clientId, config.key].join('') + + const signature = crypto + .createHmac('sha256', Buffer.from(config.secret)) + .update(msg) + .digest('hex') + .toLowerCase() + + const signedData = _.merge(data, { + key: config.key, + signature, + nonce + }) + + return request(path, 'POST', signedData) +} + +function buildMarket (fiatCode, cryptoCode) { + if (!_.includes(cryptoCode, ['BTC', 'ETH', 'LTC', 'BCH'])) { + throw new Error(`Unsupported crypto: ${cryptoCode}`) + } + + if (!_.includes(fiatCode, ['USD', 'CAD'])) { + throw new Error(`Unsupported fiat: ${fiatCode}`) + } + + let market = `${cryptoCode.toLowerCase()}_${fiatCode.toLowerCase()}` + + if (fiatCode === 'USD' && cryptoCode !== 'BTC') { + throw new Error(`Unsupported market: ${market}`) + } + + return market +} + +function request (path, method, data) { + const options = { + method, + data, + url: API_ENDPOINT + path, + headers: { + 'User-Agent': 'Mozilla/4.0 (compatible; Lamassu client)', + 'Content-Type': 'application/json; charset=utf-8' + } + } + + return axios(options) + .then(r => { + if (r.data.error) throw new Error(r.data.error.message) + return r.data + }) +} + +module.exports = { + buildMarket, + authRequest, + request +} diff --git a/lib/plugins/exchange/quadrigacx/quadrigacx.js b/lib/plugins/exchange/quadrigacx/quadrigacx.js new file mode 100644 index 00000000..8ee62b3b --- /dev/null +++ b/lib/plugins/exchange/quadrigacx/quadrigacx.js @@ -0,0 +1,28 @@ +const common = require('../../common/quadrigacx') +const coinUtils = require('../../../coin-utils') + +function buy (account, cryptoAtoms, fiatCode, cryptoCode) { + return trade('buy', account, cryptoAtoms, fiatCode, cryptoCode) +} + +function sell (account, cryptoAtoms, fiatCode, cryptoCode) { + return trade('sell', account, cryptoAtoms, fiatCode, cryptoCode) +} + +function trade (type, account, cryptoAtoms, fiatCode, cryptoCode) { + return Promise.resolve() + .then(() => { + const market = common.buildMarket(fiatCode, cryptoCode) + const options = { + book: market, + amount: coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(8) + } + + return common.authRequest(account, '/' + type, options) + }) +} + +module.exports = { + buy, + sell +} diff --git a/lib/plugins/ticker/quadrigacx/quadrigacx.js b/lib/plugins/ticker/quadrigacx/quadrigacx.js new file mode 100644 index 00000000..d9acd2c5 --- /dev/null +++ b/lib/plugins/ticker/quadrigacx/quadrigacx.js @@ -0,0 +1,49 @@ +const axios = require('axios') +const _ = require('lodash/fp') + +const BN = require('../../../bn') +const common = require('../../common/quadrigacx') + +exports.NAME = 'QuadrigaCX' +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 (fiatCode === 'USD' && cryptoCode === 'BTC' || fiatCode === 'CAD') { + return getCurrencyRates(fiatCode, cryptoCode) + } + + return axios.get('https://bitpay.com/api/rates') + .then(response => { + const fxRates = response.data + const cadRate = findCurrency(fxRates, 'CAD') + const fxRate = findCurrency(fxRates, fiatCode).div(cadRate) + + return getCurrencyRates('CAD', cryptoCode) + .then(res => ({ + rates: { + ask: res.rates.ask.times(fxRate), + bid: res.rates.bid.times(fxRate) + } + })) + }) +} + +function getCurrencyRates (fiatCode, cryptoCode) { + return Promise.resolve() + .then(() => { + const market = common.buildMarket(fiatCode, cryptoCode) + return common.request(`/ticker?book=${market}`, 'GET') + }) + .then(r => ({ + rates: { + ask: BN(r.ask), + bid: BN(r.bid) + } + })) +} diff --git a/public/elm.js b/public/elm.js index b244ad55..5674f4a2 100644 --- a/public/elm.js +++ b/public/elm.js @@ -5759,11 +5759,17 @@ var _elm_lang$core$Platform$Router = {ctor: 'Router'}; var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode = _elm_lang$core$Json_Decode$succeed; var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$resolve = _elm_lang$core$Json_Decode$andThen(_elm_lang$core$Basics$identity); -var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$custom = _elm_lang$core$Json_Decode$map2( - F2( - function (x, y) { - return y(x); - })); +var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$custom = F2( + function (decoder, wrapped) { + return A3( + _elm_lang$core$Json_Decode$map2, + F2( + function (x, y) { + return x(y); + }), + wrapped, + decoder); + }); var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$hardcoded = function (_p0) { return _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$custom( _elm_lang$core$Json_Decode$succeed(_p0)); @@ -5795,7 +5801,15 @@ var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$optionalDecoder = F3( return _elm_lang$core$Json_Decode$fail(_p2._0); } } else { - return _elm_lang$core$Json_Decode$succeed(fallback); + var _p3 = A2( + _elm_lang$core$Json_Decode$decodeValue, + _elm_lang$core$Json_Decode$keyValuePairs(_elm_lang$core$Json_Decode$value), + input); + if (_p3.ctor === 'Ok') { + return _elm_lang$core$Json_Decode$succeed(fallback); + } else { + return _elm_lang$core$Json_Decode$fail(_p3._0); + } } }; return A2(_elm_lang$core$Json_Decode$andThen, handleResult, _elm_lang$core$Json_Decode$value); @@ -37734,19 +37748,28 @@ var _user$project$NavBar$view = F2( 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/quadrigacx.json b/schemas/quadrigacx.json new file mode 100644 index 00000000..f6cb4481 --- /dev/null +++ b/schemas/quadrigacx.json @@ -0,0 +1,27 @@ +{ + "code": "quadrigacx", + "display": "QuadrigaCX", + "fields": [ + { + "code": "clientId", + "display": "Client ID", + "fieldType": "string", + "required": true, + "value": "" + }, + { + "code": "key", + "display": "API key", + "fieldType": "string", + "required": true, + "value": "" + }, + { + "code": "secret", + "display": "API secret", + "fieldType": "password", + "required": true, + "value": "" + } + ] +}