diff --git a/dev/coinatmradar.js b/dev/coinatmradar.js new file mode 100644 index 00000000..dda77018 --- /dev/null +++ b/dev/coinatmradar.js @@ -0,0 +1,16 @@ +const car = require('../lib/coinatmradar/coinatmradar') +const plugins = require('../lib/plugins') + +require('../lib/settings-loader').loadLatest() + .then(settings => { + const pi = plugins(settings) + const config = settings.config + + return pi.getRates() + .then(rates => { + return car.update({rates, config}) + .then(require('../lib/pp')('DEBUG100')) + .catch(console.log) + .then(() => process.exit()) + }) + }) diff --git a/dev/coinatmradarserver.js b/dev/coinatmradarserver.js new file mode 100644 index 00000000..afa2b101 --- /dev/null +++ b/dev/coinatmradarserver.js @@ -0,0 +1,15 @@ +const express = require('express') +const app = express() +const bodyParser = require('body-parser') + +app.use(bodyParser.raw({type: '*/*'})) + +app.post('/api/lamassu', (req, res) => { + console.log(req.headers) + console.log(req.body.toString()) + res.send('Hello World!') +}) + +app.listen(3200, () => console.log('Example app listening on port 3200!')) + +// "url": "https://coinatmradar.info/api/lamassu/" diff --git a/dev/config.js b/dev/config.js new file mode 100644 index 00000000..51dc2c3e --- /dev/null +++ b/dev/config.js @@ -0,0 +1,8 @@ +const settingsLoader = require('../lib/settings-loader') +const configManager = require('../lib/config-manager') + +settingsLoader.loadLatest() + .then(settings => { + const config = settings.config + require('../lib/pp')('config')(configManager.all('cryptoCurrencies', config)) + }) diff --git a/dev/plugins.js b/dev/plugins.js new file mode 100644 index 00000000..aea224a3 --- /dev/null +++ b/dev/plugins.js @@ -0,0 +1,10 @@ +const plugins = require('../lib/plugins') +const settingsLoader = require('../lib/settings-loader') +const pp = require('../lib/pp') + +settingsLoader.loadLatest() + .then(settings => { + console.log('DEBUG300') + const pi = plugins(settings) + pi.getRates().then(r => console.log(JSON.stringify(r))) + }) diff --git a/lamassu-schema.json b/lamassu-schema.json index 8b7281b7..f3a6f5a5 100644 --- a/lamassu-schema.json +++ b/lamassu-schema.json @@ -82,6 +82,16 @@ "hardLimitVerificationThreshold" ] }, + { + "code": "coinAtmRadar", + "display": "Coin ATM Radar", + "cryptoScope": "global", + "machineScope": "global", + "fields": [ + "coinAtmRadarActive", + "coinAtmRadarShowRates" + ] + }, { "code": "walletSettings", "display": "Wallet Settings", @@ -756,6 +766,27 @@ "notificationsEmailEnabled" ], "fieldValidation": [{"code": "required"}] + }, + { + "code": "coinAtmRadarActive", + "displayBottom": "Active", + "displayTopCount": 0, + "fieldType": "onOff", + "fieldClass": null, + "fieldValidation": [{"code": "required"}], + "default": false + }, + { + "code": "coinAtmRadarShowRates", + "displayBottom": "Show Rates", + "displayTopCount": 0, + "fieldType": "onOff", + "fieldClass": null, + "enabledIfAny": [ + "coinAtmRadarActive" + ], + "fieldValidation": [{"code": "required"}], + "default": true } ] } diff --git a/lib/coinatmradar/coinatmradar.js b/lib/coinatmradar/coinatmradar.js new file mode 100644 index 00000000..4639cca8 --- /dev/null +++ b/lib/coinatmradar/coinatmradar.js @@ -0,0 +1,124 @@ +const axios = require('axios') +const _ = require('lodash/fp') + +const db = require('../db') +const configManager = require('../config-manager') +const options = require('../options') +const logger = require('../logger') + +const TIMEOUT = 10000 +const MAX_CONTENT_LENGTH = 2000 + +// How long a machine can be down before it's considered offline +const STALE_INTERVAL = '2 minutes' + +module.exports = {update, mapRecord} + +function mapCoin (info, deviceId, cryptoCode) { + const config = info.config + const rates = info.rates[cryptoCode] || {cashIn: null, cashOut: null} + const cryptoConfig = configManager.scoped(cryptoCode, deviceId, config) + const unscoped = configManager.unscoped(config) + const showRates = unscoped.coinAtmRadarShowRates + + const cashInFee = showRates ? cryptoConfig.cashInCommission / 100 : null + const cashOutFee = showRates ? cryptoConfig.cashOutCommission / 100 : null + const cashInRate = showRates ? rates.cashIn.toNumber() : null + const cashOutRate = showRates ? rates.cashOut.toNumber() : null + + return { + cryptoCode, + cashInFee, + cashOutFee, + cashInRate, + cashOutRate + } +} + +function mapIdentification (info, deviceId) { + const machineConfig = configManager.machineScoped(deviceId, info.config) + + return { + isPhone: machineConfig.smsVerificationActive, + isPalmVein: false, + isPhoto: false, + isIdDocScan: machineConfig.idCardDataVerificationActive, + isFingerprint: false + } +} + +function mapMachine (info, machineRow) { + const deviceId = machineRow.device_id + const config = info.config + const machineConfig = configManager.machineScoped(deviceId, config) + + const lastOnline = machineRow.last_online.toISOString() + const status = machineRow.stale ? 'online' : 'offline' + + const cashLimit = machineConfig.hardLimitVerificationActive + ? machineConfig.hardLimitVerificationThreshold + : Infinity + + const cryptoCurrencies = machineConfig.cryptoCurrencies + const identification = mapIdentification(info, deviceId) + const coins = _.map(_.partial(mapCoin, [info, deviceId]), cryptoCurrencies) + + return { + machineId: deviceId, + status, + lastOnline, + cashIn: true, + cashOut: machineConfig.cashOutEnabled, + manufacturer: 'lamassu', + cashInTxLimit: cashLimit, + cashOutTxLimit: cashLimit, + cashInDailyLimit: cashLimit, + cashOutDailyLimit: cashLimit, + fiatCurrency: machineConfig.fiatCurrency, + identification, + coins + } +} + +function getMachines (info) { + const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices + where display=TRUE and + paired=TRUE + order by created` + + return db.any(sql, [STALE_INTERVAL]) + .then(_.map(_.partial(mapMachine, [info]))) +} + +function sendRadar (data) { + const config = { + url: options.coinAtmRadar.url, + method: 'post', + data, + timeout: TIMEOUT, + maxContentLength: MAX_CONTENT_LENGTH + } + + return axios(config) + .then(r => console.log(r.status)) +} + +function mapRecord (info) { + const timestamp = new Date().toISOString() + return getMachines(info) + .then(machines => ({ + operatorId: options.operatorId, + timestamp, + machines + })) +} + +function update (info) { + const config = configManager.unscoped(info.config) + + if (!config.coinAtmRadarActive) return Promise.resolve() + + return mapRecord(info) + .then(sendRadar) + .catch(err => logger.error(err)) +} diff --git a/lib/config-manager.js b/lib/config-manager.js index 35a656a6..bbf94c9c 100644 --- a/lib/config-manager.js +++ b/lib/config-manager.js @@ -5,7 +5,8 @@ module.exports = { cryptoScoped, machineScoped, scoped, - scopedValue + scopedValue, + all } function matchesValue (crypto, machine, instance) { @@ -60,3 +61,7 @@ function cryptoScoped (crypto, config) { function scoped (crypto, machine, config) { return generalScoped(crypto, machine, config) } + +function all (code, config) { + return _.uniq(_.map('fieldValue.value', _.filter(i => i.fieldLocator.code === code, config))) +} diff --git a/lib/config-validate.js b/lib/config-validate.js index e20151ae..6fb229dc 100644 --- a/lib/config-validate.js +++ b/lib/config-validate.js @@ -144,8 +144,6 @@ function ensureConstraints (config) { }) } -const pp = require('./pp') - function validateRequires (config) { return fetchMachines() .then(machineList => { diff --git a/lib/plugins.js b/lib/plugins.js index 35b969a7..6cb90db2 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -50,8 +50,8 @@ function plugins (settings, deviceId) { if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) const rate = rateRec.rates rates[cryptoCode] = { - cashIn: rate.ask.mul(cashInCommission), - cashOut: cashOutCommission && rate.bid.div(cashOutCommission) + cashIn: rate.ask.mul(cashInCommission).round(5), + cashOut: cashOutCommission && rate.bid.div(cashOutCommission).round(5) } }) @@ -620,7 +620,18 @@ function plugins (settings, deviceId) { return machineLoader.getMachineNames(settings.config) } + function getRates () { + const config = configManager.unscoped(settings.config) + const cryptoCodes = _.flatten(configManager.all('cryptoCurrencies', settings.config)) + const fiatCode = config.fiatCurrency + const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) + + return Promise.all(tickerPromises) + .then(buildRates) + } + return { + getRates, pollQueries, sendCoins, newAddress, diff --git a/lib/poller.js b/lib/poller.js index acddaf37..f9893a1a 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -6,6 +6,7 @@ const cashOutTx = require('./cash-out/cash-out-tx') const cashInTx = require('./cash-in/cash-in-tx') const sanctionsUpdater = require('./ofac/update') const sanctions = require('./ofac/index') +const coinAtmRadar = require('./coinatmradar/coinatmradar') const INCOMING_TX_INTERVAL = 30 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds @@ -15,6 +16,7 @@ const TRADE_INTERVAL = 60 * T.seconds const PONG_INTERVAL = 10 * T.seconds const PONG_CLEAR_INTERVAL = 1 * T.day const SANCTIONS_UPDATE_INTERVAL = 1 * T.week +const RADAR_UPDATE_INTERVAL = 5 * T.minutes const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds @@ -38,6 +40,17 @@ function updateAndLoadSanctions () { .then(() => logger.info('Sanctions database updated.')) } +const pp = require('./pp') +function updateCoinAtmRadar () { + const config = settings().config + + return pi().getRates() + .then(rates => { + pp('DEBUG200')(rates) + return coinAtmRadar.update({rates, config}) + }) +} + function start (__settings) { reload(__settings) @@ -49,6 +62,7 @@ function start (__settings) { cashOutTx.monitorUnnotified(settings()) pi().sweepHd() notifier.checkNotification(pi()) + updateCoinAtmRadar() setInterval(() => pi().executeTrades(), TRADE_INTERVAL) setInterval(() => cashOutTx.monitorLiveIncoming(settings()), LIVE_INCOMING_TX_INTERVAL) @@ -60,6 +74,7 @@ function start (__settings) { setInterval(() => pi().pongClear(), PONG_CLEAR_INTERVAL) setInterval(() => notifier.checkNotification(pi()), CHECK_NOTIFICATION_INTERVAL) setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL) + setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL) } module.exports = {start, reload} diff --git a/migrations/1526038623129-add_device_location.js b/migrations/1526038623129-add_device_location.js new file mode 100644 index 00000000..a1adf5ca --- /dev/null +++ b/migrations/1526038623129-add_device_location.js @@ -0,0 +1,13 @@ +var db = require('./db') + +exports.up = function (next) { + var sql = [ + 'alter table devices add column last_online timestamptz not null default now()', + "alter table devices add column location json not null default '{}'" + ] + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/package.json b/package.json index 723f232a..a144c00c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@fczbkk/uuid4": "^3.0.0", "axios": "^0.16.1", "base-x": "^3.0.2", - "bignumber.js": "^4.0.2", + "bignumber.js": "^4.1.0", "bip39": "^2.3.1", "bitcoind-rpc": "^0.7.0", "bitcore-lib": "^0.15.0", diff --git a/public/elm.js b/public/elm.js index 9758df59..8bfa22a8 100644 --- a/public/elm.js +++ b/public/elm.js @@ -36714,7 +36714,11 @@ var _user$project$NavBar$determineConfigCategory = function (configCode) { _1: { ctor: '::', _0: 'compliance', - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: 'coinAtmRadar', + _1: {ctor: '[]'} + } } } }) ? _elm_lang$core$Maybe$Just(_user$project$CoreTypes$GlobalSettingsCat) : _elm_lang$core$Maybe$Nothing); @@ -37155,7 +37159,11 @@ var _user$project$NavBar$view = F2( _1: { ctor: '::', _0: A2(configLink, 'compliance', 'Compliance'), - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: A2(configLink, 'coinAtmRadar', 'Coin ATM Radar'), + _1: {ctor: '[]'} + } } } }),