diff --git a/lib/admin/config.js b/lib/admin/config.js index c921a369..0ab4591c 100644 --- a/lib/admin/config.js +++ b/lib/admin/config.js @@ -11,6 +11,7 @@ const settingsLoader = require('../settings-loader') const db = require('../db') const options = require('../options') const configManager = require('../config-manager') +const configValidate = require('../config-validate') const machines = require('./machines') @@ -38,15 +39,6 @@ function allScopes (cryptoScopes, machineScopes) { return scopes } -function allCryptoScopes (cryptos, cryptoScope) { - const cryptoScopes = [] - - if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global') - if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r)) - - return cryptoScopes -} - function allMachineScopes (machineList, machineScope) { const machineScopes = [] @@ -56,43 +48,6 @@ function allMachineScopes (machineList, machineScope) { return machineScopes } -function satisfiesRequire (config, cryptos, machineList, field, refFields) { - const fieldCode = field.code - - const scopes = allScopes( - allCryptoScopes(cryptos, field.cryptoScope), - allMachineScopes(machineList, field.machineScope) - ) - - return scopes.every(scope => { - const isEnabled = () => refFields.some(refField => { - return isScopeEnabled(config, cryptos, machineList, refField, scope) - }) - - const isBlank = () => R.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config)) - const isRequired = refFields.length === 0 || isEnabled() - - return isRequired ? !isBlank() : true - }) -} - -function isScopeEnabled (config, cryptos, machineList, refField, scope) { - const [cryptoScope, machineScope] = scope - const candidateCryptoScopes = cryptoScope === 'global' - ? allCryptoScopes(cryptos, refField.cryptoScope) - : [cryptoScope] - - const candidateMachineScopes = machineScope === 'global' - ? allMachineScopes(machineList, refField.machineScope) - : [ machineScope ] - - const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes) - const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config) - const values = allRefCandidateScopes.map(getFallbackValue) - - return values.some(r => r) -} - function getCryptos (config, machineList) { const scopes = allScopes(['global'], allMachineScopes(machineList, 'both')) const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config) @@ -112,60 +67,9 @@ function getField (schema, group, fieldCode) { const fetchMachines = () => machines.getMachines() .then(machineList => machineList.map(r => r.deviceId)) -function validateFieldParameter (value, validator) { - switch (validator.code) { - case 'required': - return true // We don't validate this here - case 'min': - return value >= validator.min - case 'max': - return value <= validator.max - default: - throw new Error('Unknown validation type: ' + validator.code) - } -} - -// Validates specific field properties other than required property -function enforceValidConfigParameters (fieldInstances) { - return fetchSchema() - .then(schema => { - const pickField = fieldCode => schema.fields.find(r => r.code === fieldCode) - - return fieldInstances.every(fieldInstance => { - const fieldCode = fieldInstance.fieldLocator.code - const field = pickField(fieldCode) - const fieldValue = fieldInstance.fieldValue - - const isValid = field.fieldValidation - .every(validator => validateFieldParameter(fieldValue.value, validator)) - - if (isValid) return true - - throw new Error('Invalid config value') - }) - }) -} - -function validateConfig (config) { - return Promise.all([fetchSchema(), fetchMachines()]) - .then(([schema, machineList]) => { - const cryptos = getCryptos(config, machineList) - return schema.groups.filter(group => { - return group.fields.some(fieldCode => { - const field = getField(schema, group, fieldCode) - if (!field.fieldValidation.find(r => r.code === 'required')) return false - - const refFields = (field.enabledIf || []).map(R.curry(getField)(schema, null)) - return !satisfiesRequire(config, cryptos, machineList, field, refFields) - }) - }) - }) - .then(arr => arr.map(r => r.code)) -} - function validateCurrentConfig () { return fetchConfig() - .then(validateConfig) + .then(configValidate.validateRequires) } function fetchConfigGroup (code) { @@ -245,6 +149,7 @@ function fetchData () { {code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']}, {code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']}, {code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ['BTC', 'ETH']}, + {code: 'no-exchange', display: 'No exchange', class: 'exchange', cryptos: ['BTC', 'ETH']}, {code: 'mock-exchange', display: 'Mock exchange', class: 'exchange', cryptos: ['BTC', 'ETH']}, {code: 'mock-sms', display: 'Mock SMS', class: 'sms'}, {code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'}, @@ -258,7 +163,7 @@ function fetchData () { function saveConfigGroup (results) { if (results.values.length === 0) return fetchConfigGroup(results.groupCode) - return enforceValidConfigParameters(results.values) + return configValidate.ensureConstraints(results.values) .then(fetchConfig) .then(oldValues => { results.values.forEach(newValue => { diff --git a/lib/admin/server.js b/lib/admin/server.js index b0c51d87..1b8cf6ee 100644 --- a/lib/admin/server.js +++ b/lib/admin/server.js @@ -45,19 +45,26 @@ function status () { const lastPing = statusRow && age.humanize() return settingsLoader.loadLatest() + .catch(() => null) .then(settings => { - return ticker.getRates(settings, 'USD', 'BTC') - .then(ratesRec => { - const rates = [{ - crypto: 'BTC', - bid: parseFloat(ratesRec.rates.bid), - ask: parseFloat(ratesRec.rates.ask) - }] - return {up, lastPing, rates, machineStatus} - }) - .catch(() => ({up, lastPing, rates: [], machineStatus})) + return getRates(settings) + .then(rates => ({up, lastPing, rates, machineStatus})) }) }) } +function getRates (settings) { + if (!settings) return Promise.resolve([]) + + return ticker.getRates(settings, 'USD', 'BTC') + .then(ratesRec => { + return [{ + crypto: 'BTC', + bid: parseFloat(ratesRec.rates.bid), + ask: parseFloat(ratesRec.rates.ask) + }] + }) + .catch(() => []) +} + module.exports = {status} diff --git a/lib/app.js b/lib/app.js index 08be5368..9d16e5ed 100644 --- a/lib/app.js +++ b/lib/app.js @@ -6,7 +6,6 @@ const argv = require('minimist')(process.argv.slice(2)) const routes = require('./routes') const logger = require('./logger') const poller = require('./poller') -const verifySchema = require('./verify-schema') const settingsLoader = require('./settings-loader') const options = require('./options') @@ -33,8 +32,7 @@ function run () { } function runOnce () { - return verifySchema.valid() - .then(() => settingsLoader.loadLatest()) + return settingsLoader.loadLatest() .then(settings => { poller.start(settings) diff --git a/lib/cash-in-tx.js b/lib/cash-in-tx.js index 144195e7..03cdd095 100644 --- a/lib/cash-in-tx.js +++ b/lib/cash-in-tx.js @@ -19,7 +19,6 @@ function post (tx, pi) { const isolationLevel = pgp.txMode.isolationLevel const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) - console.log('DEBUG502: %j', tx) function transaction (t) { const sql = 'select * from cash_in_txs where id=$1' const sql2 = 'select * from bills where cash_in_txs_id=$1' @@ -131,7 +130,6 @@ function insertNewBills (billRows, tx) { function upsert (oldTx, tx) { if (!oldTx) { - console.log('DEBUG500: %j', tx) return insert(tx) .then(newTx => [oldTx, newTx]) } @@ -160,9 +158,7 @@ function update (tx, changes) { } function registerTrades (pi, txVector) { - console.log('DEBUG400') const newBills = _.last(txVector) - console.log('DEBUG401: %j', newBills) _.forEach(bill => pi.buy(bill), newBills) } diff --git a/lib/cash-out-tx.js b/lib/cash-out-tx.js index 4e9c9116..8cca541f 100644 --- a/lib/cash-out-tx.js +++ b/lib/cash-out-tx.js @@ -36,7 +36,6 @@ function httpError (msg, code) { } function post (tx, pi) { - console.log('DEBUG101: %j', tx) const TransactionMode = pgp.txMode.TransactionMode const isolationLevel = pgp.txMode.isolationLevel const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) @@ -104,7 +103,6 @@ function logAction (action, _rec, tx) { const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem}) const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') - console.log('DEBUG110: %j', sql) return db.none(sql) .then(_.constant(tx)) } @@ -204,9 +202,7 @@ function update (tx, changes) { } function nextHd (isHd, tx) { - console.log('DEBUG160: %s', isHd) if (!isHd) return Promise.resolve(tx) - console.log('DEBUG161: %s', isHd) return db.one("select nextval('hd_indices_seq') as hd_index") .then(row => _.set('hdIndex', row.hd_index, tx)) @@ -257,9 +253,7 @@ function preProcess (oldTx, newTx, pi) { return logAction(updatedTx.status, rec, updatedTx) } - console.log('DEBUG120: %j', [oldTx, updatedTx]) if (!oldTx.dispenseConfirmed && updatedTx.dispenseConfirmed) { - console.log('DEBUG121') return logDispense(updatedTx) .then(updateCassettes(updatedTx)) } @@ -280,7 +274,6 @@ function postProcess (txVector, pi) { const [oldTx, newTx] = txVector if (newTx.dispense && !oldTx.dispense) { - console.log('DEBUG130') return pi.buildCassettes() .then(cassettes => { pi.sell(newTx) diff --git a/lib/config-validate.js b/lib/config-validate.js new file mode 100644 index 00000000..31a92734 --- /dev/null +++ b/lib/config-validate.js @@ -0,0 +1,150 @@ +const _ = require('lodash/fp') + +const configManager = require('./config-manager') +const machines = require('./admin/machines') +const schema = require('../lamassu-schema.json') + +function allScopes (cryptoScopes, machineScopes) { + const scopes = [] + cryptoScopes.forEach(c => { + machineScopes.forEach(m => scopes.push([c, m])) + }) + + return scopes +} + +function allCryptoScopes (cryptos, cryptoScope) { + const cryptoScopes = [] + + if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global') + if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r)) + + return cryptoScopes +} + +function allMachineScopes (machineList, machineScope) { + const machineScopes = [] + + if (machineScope === 'global' || machineScope === 'both') machineScopes.push('global') + if (machineScope === 'specific' || machineScope === 'both') machineList.forEach(r => machineScopes.push(r)) + + return machineScopes +} + +function satisfiesRequire (config, cryptos, machineList, field, refFields) { + const fieldCode = field.code + + const scopes = allScopes( + allCryptoScopes(cryptos, field.cryptoScope), + allMachineScopes(machineList, field.machineScope) + ) + + return scopes.every(scope => { + const isEnabled = () => refFields.some(refField => { + return isScopeEnabled(config, cryptos, machineList, refField, scope) + }) + + const isBlank = () => _.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config)) + const isRequired = refFields.length === 0 || isEnabled() + + return isRequired ? !isBlank() : true + }) +} + +function isScopeEnabled (config, cryptos, machineList, refField, scope) { + const [cryptoScope, machineScope] = scope + const candidateCryptoScopes = cryptoScope === 'global' + ? allCryptoScopes(cryptos, refField.cryptoScope) + : [cryptoScope] + + const candidateMachineScopes = machineScope === 'global' + ? allMachineScopes(machineList, refField.machineScope) + : [ machineScope ] + + const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes) + const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config) + const values = allRefCandidateScopes.map(getFallbackValue) + + return values.some(r => r) +} + +function getCryptos (config, machineList) { + const scopes = allScopes(['global'], allMachineScopes(machineList, 'both')) + const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config) + return scopes.reduce((acc, scope) => _.union(acc, scoped(scope)), []) +} + +function getGroup (fieldCode) { + return _.find(group => _.includes(fieldCode, group.fields), schema.groups) +} + +function getField (group, fieldCode) { + if (!group) group = getGroup(fieldCode) + const field = _.find(_.matchesProperty('code', fieldCode), schema.fields) + return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field) +} + +const fetchMachines = () => machines.getMachines() +.then(machineList => machineList.map(r => r.deviceId)) + +function validateFieldParameter (value, validator) { + switch (validator.code) { + case 'required': + return true // We don't validate this here + case 'min': + return value >= validator.min + case 'max': + return value <= validator.max + default: + throw new Error('Unknown validation type: ' + validator.code) + } +} + +function ensureConstraints (config) { + const pickField = fieldCode => schema.fields.find(r => r.code === fieldCode) + + return Promise.resolve() + .then(() => { + config.every(fieldInstance => { + const fieldCode = fieldInstance.fieldLocator.code + const field = pickField(fieldCode) + const fieldValue = fieldInstance.fieldValue + + const isValid = field.fieldValidation + .every(validator => validateFieldParameter(fieldValue.value, validator)) + + if (isValid) return true + throw new Error('Invalid config value') + }) + }) +} + +function validateRequires (config) { + return fetchMachines() + .then(machineList => { + const cryptos = getCryptos(config, machineList) + + return schema.groups.filter(group => { + return group.fields.some(fieldCode => { + const field = getField(group, fieldCode) + if (!field.fieldValidation.find(r => r.code === 'required')) return false + + const refFields = _.map(_.partial(getField, null), field.enabledIf) + return !satisfiesRequire(config, cryptos, machineList, field, refFields) + }) + }) + }) + .then(arr => arr.map(r => r.code)) +} + +function validate (config) { + return Promise.resolve() + .then(() => ensureConstraints(config)) + .then(() => validateRequires(config)) + .then(arr => { + if (arr.length === 0) return config + throw new Error('Invalid configuration') + }) +} + +module.exports = {validate, ensureConstraints, validateRequires} diff --git a/lib/exchange.js b/lib/exchange.js index 02cc5b3d..77f6f088 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -2,13 +2,16 @@ const configManager = require('./config-manager') const ph = require('./plugin-helper') function lookupExchange (settings, cryptoCode) { - return configManager.cryptoScoped(cryptoCode, settings.config).exchange + const exchange = configManager.cryptoScoped(cryptoCode, settings.config).exchange + if (exchange === 'no-exchange') return null + return exchange } function fetchExchange (settings, cryptoCode) { return Promise.resolve() .then(() => { const plugin = lookupExchange(settings, cryptoCode) + if (!plugin) throw new Error('No exchange set') const exchange = ph.load(ph.EXCHANGE, plugin) const account = settings.accounts[plugin] diff --git a/lib/notifier.js b/lib/notifier.js index ccd912b4..e53b662a 100644 --- a/lib/notifier.js +++ b/lib/notifier.js @@ -54,7 +54,6 @@ function sendNoAlerts (plugins) { function checkNotification (plugins) { return checkStatus(plugins) .then(alertRec => { - console.log('DEBUG445: %j', alertRec) const currentAlertFingerprint = buildAlertFingerprint(alertRec) if (!currentAlertFingerprint) { const inAlert = !!alertFingerprint @@ -80,7 +79,6 @@ function checkNotification (plugins) { alertFingerprint = currentAlertFingerprint lastAlertTime = Date.now() - console.log('DEBUG446: %j', rec) return plugins.sendMessage(rec) }) .then(results => { diff --git a/lib/plugins.js b/lib/plugins.js index faad0d39..e9db8b28 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -119,6 +119,7 @@ function plugins (settings, deviceId) { function fetchCurrentConfigVersion () { const sql = `select id from user_config where type=$1 + and valid order by id desc limit 1` @@ -278,9 +279,7 @@ function plugins (settings, deviceId) { const market = [fiatCode, cryptoCode].join('') - console.log('DEBUG333') if (!exchange.active(settings, cryptoCode)) return - console.log('DEBUG334') logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) if (!tradesQueues[market]) tradesQueues[market] = [] @@ -485,7 +484,6 @@ function plugins (settings, deviceId) { function sweepHdRow (row) { const cryptoCode = row.crypto_code - console.log('DEBUG200') return wallet.sweep(settings, cryptoCode, row.hd_index) .then(txHash => { if (txHash) { diff --git a/lib/plugins/sms/twilio/twilio.js b/lib/plugins/sms/twilio/twilio.js index 5c6b6ea6..76371804 100644 --- a/lib/plugins/sms/twilio/twilio.js +++ b/lib/plugins/sms/twilio/twilio.js @@ -17,11 +17,9 @@ function sendMessage (account, rec) { from: account.fromNumber } - console.log('DEBUG111: %j', opts) return client.sendMessage(opts) .catch(err => { - console.log('DEBUG113: %s', err) if (_.includes(err.code, BAD_NUMBER_CODES)) { const badNumberError = new Error(err.message) badNumberError.name = 'BadNumberError' @@ -30,7 +28,6 @@ function sendMessage (account, rec) { throw new Error(err.message) }) - .then(_.tap(() => console.log('DEBUG112'))) } module.exports = { diff --git a/lib/routes.js b/lib/routes.js index 93b44f47..24a3e4e7 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -90,7 +90,6 @@ function getTx (req, res, next) { function getPhoneTx (req, res, next) { if (req.query.phone) { - console.log('DEBUG120: %s', req.query.phone) return helpers.fetchPhoneTx(req.query.phone) .then(r => res.json(r)) .catch(next) @@ -322,7 +321,6 @@ let oldVersionId = 'initial' function populateSettings (req, res, next) { const versionId = req.headers['config-version'] if (versionId !== oldVersionId) { - console.log('DEBUG611: %s', versionId) oldVersionId = versionId } diff --git a/lib/settings-loader.js b/lib/settings-loader.js index cb962612..da8d9b55 100644 --- a/lib/settings-loader.js +++ b/lib/settings-loader.js @@ -6,11 +6,7 @@ const argv = require('minimist')(process.argv.slice(2)) const pify = require('pify') const db = require('./db') -const options = require('./options') -const logger = require('./logger') - -const schemaPath = path.resolve(options.lamassuServerPath, 'lamassu-schema.json') -const schema = require(schemaPath) +const configValidate = require('./config-validate') let settingsCache @@ -69,17 +65,25 @@ function loadConfig (versionId) { const sql = `select data from user_config - where id=$1 and type=$2` + where id=$1 and type=$2 + and valid` - return db.oneOrNone(sql, [versionId, 'config']) - .then(row => row ? row.data.config : []) - .then(validate) + return db.one(sql, [versionId, 'config']) + .then(row => row.data.config) + .then(configValidate.validate) + .catch(err => { + if (err.name === 'QueryResultError') { + throw new Error('No such config version: ' + versionId) + } + + throw err + }) } function loadLatestConfig () { if (argv.fixture) return loadFixture() - const sql = `select data + const sql = `select id, valid, data from user_config where type=$1 and valid @@ -88,7 +92,7 @@ function loadLatestConfig () { return db.one(sql, ['config']) .then(row => row.data.config) - .then(validate) + .then(configValidate.validate) .catch(err => { if (err.name === 'QueryResultError') { throw new Error('lamassu-server is not configured') @@ -98,38 +102,6 @@ function loadLatestConfig () { }) } -function checkConstraint (entry, constraint) { - switch (constraint.code) { - case 'min': - return entry.fieldValue.value >= constraint.min - default: - return true - } -} - -function validateConstraint (entry, constraint) { - const isValid = checkConstraint(entry, constraint) - if (!isValid) logger.error(`Validation error: ${entry.fieldLocator.code} [${constraint.code}]`) - return isValid -} - -function validateEntry (entry) { - const fieldCode = entry.fieldLocator.code - const schemaEntry = _.find(_.matchesProperty('code', fieldCode), schema.fields) - if (!schemaEntry) throw new Error(`Unsupported field: ${fieldCode}`) - - const validations = schemaEntry.fieldValidation - return _.every(constraint => validateConstraint(entry, constraint), validations) -} - -function isValid (config) { - return _.every(validateEntry, config) -} -function validate (config) { - if (!isValid(config)) throw new Error('Invalid config') - return config -} - function loadAccounts () { const toFields = fieldArr => _.fromPairs(_.map(r => [r.code, r.value], fieldArr)) const toPairs = r => [r.code, toFields(r.fields)] @@ -147,7 +119,10 @@ function settings () { function save (config) { const sql = 'insert into user_config (type, data, valid) values ($1, $2, $3)' - return db.none(sql, ['config', {config}, isValid(config)]) + + return configValidate.validate(config) + .then(() => db.none(sql, ['config', {config}, true])) + .catch(() => db.none(sql, ['config', {config}, false])) } module.exports = { diff --git a/lib/verify-schema.js b/lib/verify-schema.js deleted file mode 100644 index da184595..00000000 --- a/lib/verify-schema.js +++ /dev/null @@ -1,12 +0,0 @@ -const util = require('util') -const config = require('./admin/config') - -function valid () { - return config.validateCurrentConfig() - .then(errors => { - if (errors.length === 0) return - throw new Error('Schema validation error: ' + util.inspect(errors, {colors: true})) - }) -} - -module.exports = {valid} diff --git a/lib/wallet.js b/lib/wallet.js index bdc688b7..b500db7f 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -30,7 +30,6 @@ function fetchWallet (settings, cryptoCode) { .then(hex => { const masterSeed = Buffer.from(hex.trim(), 'hex') const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet - console.log('DEBUG555: %s', plugin) const wallet = ph.load(ph.WALLET, plugin) const account = settings.accounts[plugin] diff --git a/public/elm.js b/public/elm.js index 6bd3420d..dae5d972 100644 --- a/public/elm.js +++ b/public/elm.js @@ -32713,7 +32713,7 @@ var _user$project$TransactionTypes$CashOutTxRec = function (a) { return function (m) { return function (n) { return function (o) { - return {id: a, machineName: b, toAddress: c, cryptoAtoms: d, cryptoCode: e, fiat: f, fiatCode: g, status: h, dispensed: i, notified: j, redeemed: k, phone: l, error: m, created: n, confirmed: o}; + return {id: a, machineName: b, toAddress: c, cryptoAtoms: d, cryptoCode: e, fiat: f, fiatCode: g, status: h, dispense: i, notified: j, redeemed: k, phone: l, error: m, created: n, confirmed: o}; }; }; }; @@ -32833,7 +32833,7 @@ var _user$project$TransactionDecoder$cashOutTxDecoder = A3( _elm_lang$core$Json_Decode$bool, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'dispensed', + 'dispense', _elm_lang$core$Json_Decode$bool, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,