const _ = require('lodash/fp') const devMode = require('minimist')(process.argv.slice(2)).dev const currencies = require('../../currencies.json') const languageRec = require('../../languages.json') const countries = require('../../countries.json') const settingsLoader = require('../settings-loader') const db = require('../db') const configManager = require('../config-manager') const configValidate = require('../config-validate') const machineLoader = require('../machine-loader') const jsonSchema = require('../../lamassu-schema.json') function fetchSchema () { return _.cloneDeep(jsonSchema) } function fetchConfig () { const sql = `select data from user_config where type=$1 and schema_version=$2 order by id desc limit 1` return db.oneOrNone(sql, ['config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row ? row.data.config : []) } function allScopes (cryptoScopes, machineScopes) { const scopes = [] cryptoScopes.forEach(c => { machineScopes.forEach(m => scopes.push([c, m])) }) return scopes } 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 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 (schema, fieldCode) { return schema.groups.find(group => group.fields.find(_.isEqual(fieldCode))) } function getField (schema, group, fieldCode) { if (!group) group = getGroup(schema, fieldCode) const field = schema.fields.find(r => r.code === fieldCode) return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field) } const fetchMachines = () => machineLoader.getMachines() .then(machineList => machineList.map(r => r.deviceId)) function validateCurrentConfig () { return fetchConfig() .then(configValidate.validateRequires) } const decorateEnabledIf = _.curry((schemaFields, schemaField) => { const code = schemaField.fieldLocator.code const field = _.find(f => f.code === code, schemaFields) return _.assign(schemaField, { fieldEnabledIfAny: field.enabledIfAny || [], fieldEnabledIfAll: field.enabledIfAll || [] }) }) function fetchConfigGroup (code) { const fieldLocatorCodeEq = _.matchesProperty(['fieldLocator', 'code']) return Promise.all([fetchSchema(), fetchData(), fetchConfig(), fetchMachines()]) .then(([schema, data, config, machineList]) => { const groupSchema = schema.groups.find(r => r.code === code) if (!groupSchema) throw new Error('No such group schema: ' + code) const schemaFields = groupSchema.fields .map(_.curry(getField)(schema, groupSchema)) .map(f => _.assign(f, { fieldEnabledIfAny: f.enabledIfAny || [], fieldEnabledIfAll: f.enabledIfAll || [] })) const candidateFields = [ schemaFields.map(_.get('requiredIf')), schemaFields.map(_.get('enabledIfAny')), schemaFields.map(_.get('enabledIfAll')), groupSchema.fields, 'fiatCurrency' ] const smush = _.flow(_.flattenDeep, _.compact, _.uniq) const configFields = smush(candidateFields) // Expand this to check against full schema const fieldValidator = field => !_.isNil(_.get('fieldLocator.fieldScope.crypto', field)) const reducer = (acc, configField) => { return acc.concat(config.filter(fieldLocatorCodeEq(configField))) } const reducedFields = _.filter(fieldValidator, configFields.reduce(reducer, [])) const values = _.map(decorateEnabledIf(schema.fields), reducedFields) groupSchema.fields = undefined groupSchema.entries = schemaFields const selectedCryptos = _.defaultTo([], getCryptos(config, machineList)) return { schema: groupSchema, values, selectedCryptos, data } }) } function massageCurrencies (currencies) { const convert = r => ({ code: r['Alphabetic Code'], display: r['Currency'] }) const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD'] const mapped = _.map(convert, currencies) const codeToRec = code => _.find(_.matchesProperty('code', code), mapped) const top5 = _.map(codeToRec, top5Codes) const raw = _.uniqBy(_.get('code'), _.concat(top5, mapped)) return raw.filter(r => r.code[0] !== 'X' && r.display.indexOf('(') === -1) } const mapLanguage = lang => { const arr = lang.split('-') const code = arr[0] const country = arr[1] const langNameArr = languageRec.lang[code] if (!langNameArr) return null const langName = langNameArr[0] if (!country) return {code: lang, display: langName} return {code: lang, display: `${langName} [${country}]`} } const supportedLanguages = languageRec.supported const languages = supportedLanguages.map(mapLanguage).filter(r => r) const ALL_CRYPTOS = ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH'] const filterAccounts = (data, isDevMode) => { const notAllowed = ['mock-ticker', 'mock-wallet', 'mock-exchange', 'mock-sms', 'mock-id-verify', 'mock-zero-conf'] const filterOut = o => _.includes(o.code, notAllowed) return isDevMode ? data : {...data, accounts: _.filter(a => !filterOut(a), data.accounts)} } function fetchData () { return machineLoader.getMachineNames() .then(machineList => ({ currencies: massageCurrencies(currencies), cryptoCurrencies: [ {crypto: 'BTC', display: 'Bitcoin'}, {crypto: 'ETH', display: 'Ethereum'}, {crypto: 'LTC', display: 'Litecoin'}, {crypto: 'DASH', display: 'Dash'}, {crypto: 'ZEC', display: 'Zcash'}, {crypto: 'BCH', display: 'Bitcoin Cash'} ], languages: languages, countries, accounts: [ {code: 'bitpay', display: 'Bitpay', class: 'ticker', cryptos: ['BTC', 'BCH']}, {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', 'ZEC', 'DASH']}, {code: 'itbit', display: 'itBit', class: 'ticker', cryptos: ['BTC', 'ETH']}, {code: 'mock-ticker', display: 'Mock (Caution!)', 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}, {code: 'infura', display: 'Infura', class: 'wallet', cryptos: ['ETH']}, {code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']}, {code: 'zcashd', display: 'zcashd', class: 'wallet', cryptos: ['ZEC']}, {code: 'litecoind', display: 'litecoind', class: 'wallet', cryptos: ['LTC']}, {code: 'dashd', display: 'dashd', class: 'wallet', cryptos: ['DASH']}, {code: 'bitcoincashd', display: 'bitcoincashd', class: 'wallet', cryptos: ['BCH']}, {code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC', 'ZEC', 'LTC', 'BCH', 'DASH']}, {code: 'bitstamp', display: 'Bitstamp', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']}, {code: 'itbit', display: 'itBit', class: 'exchange', cryptos: ['BTC', 'ETH']}, {code: 'kraken', display: 'Kraken', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', '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}, {code: 'mock-sms', display: 'Mock SMS', class: 'sms'}, {code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'}, {code: 'twilio', display: 'Twilio', class: 'sms'}, {code: 'mailgun', display: 'Mailgun', class: 'email'}, {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', 'ETH']} ], machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name})) })) .then((data) => { return filterAccounts(data, devMode) }) } function saveConfigGroup (results) { if (results.values.length === 0) return fetchConfigGroup(results.groupCode) return settingsLoader.modifyConfig(results.values) .then(() => fetchConfigGroup(results.groupCode)) } module.exports = { fetchConfigGroup, saveConfigGroup, validateCurrentConfig, fetchConfig, filterAccounts }