const path = require('path') const fs = require('fs') const _ = require('lodash/fp') const argv = require('minimist')(process.argv.slice(2)) const pify = require('pify') const pgp = require('pg-promise')() const db = require('../db') const configValidate = require('./config-validate') const schema = require('./lamassu-schema.json') let settingsCache function loadFixture () { const fixture = argv.fixture const machine = argv.machine if (fixture && !machine) throw new Error('Missing --machine parameter for --fixture') const fixturePath = fixture => path.resolve(__dirname, '..', 'test', 'fixtures', fixture + '.json') const promise = fixture ? pify(fs.readFile)(fixturePath(fixture)).then(JSON.parse) : Promise.resolve([]) return promise .then(values => _.map(v => { return (v.fieldLocator.fieldScope.machine === 'machine') ? _.set('fieldLocator.fieldScope.machine', machine, v) : v }, values)) } function isEquivalentField (a, b) { return _.isEqual( [a.fieldLocator.code, a.fieldLocator.fieldScope], [b.fieldLocator.code, b.fieldLocator.fieldScope] ) } // b overrides a function mergeValues (a, b) { return _.reject(r => _.isNil(r.fieldValue), _.unionWith(isEquivalentField, b, a)) } function load (versionId) { if (!versionId) throw new Error('versionId is required') return Promise.all([loadConfig(versionId), loadAccounts()]) .then(([config, accounts]) => ({ config, accounts })) } function loadLatest (filterSchemaVersion = true) { return Promise.all([loadLatestConfig(filterSchemaVersion), loadAccounts(filterSchemaVersion)]) .then(([config, accounts]) => ({ config, accounts })) } function loadConfig (versionId) { if (argv.fixture) return loadFixture() const sql = `select data from user_config where id=$1 and type=$2 and schema_version=$3 and valid` return db.$one(sql, [versionId, 'config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION]) .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 (filterSchemaVersion = true) { if (argv.fixture) return loadFixture() const sql = `select id, valid, data from user_config where type=$1 ${filterSchemaVersion ? 'and schema_version=$2' : ''} and valid order by id desc limit 1` return db.$one(sql, ['config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row.data.config) .then(configValidate.validate) .catch(err => { if (err.name === 'QueryResultError') { throw new Error('lamassu-server is not configured') } throw err }) } function loadRecentConfig () { if (argv.fixture) return loadFixture() const sql = `select id, data from user_config where type=$1 and schema_version=$2 order by id desc limit 1` return db.$one(sql, ['config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row.data.config) } function loadAccounts (filterSchemaVersion = true) { const toFields = fieldArr => _.fromPairs(_.map(r => [r.code, r.value], fieldArr)) const toPairs = r => [r.code, toFields(r.fields)] return db.$oneOrNone(`select data from user_config where type=$1 ${filterSchemaVersion ? 'and schema_version=$2' : ''}`, ['accounts', configValidate.SETTINGS_LOADER_SCHEMA_VERSION]) .then(function (data) { if (!data) return {} return _.fromPairs(_.map(toPairs, data.data.accounts)) }) } function settings () { return settingsCache } function save (config) { const sql = 'insert into user_config (type, data, valid) values ($1, $2, $3)' return configValidate.validate(config) .then(() => db.$none(sql, ['config', {config}, true])) .catch(() => db.$none(sql, ['config', {config}, false])) } function configAddField (scope, fieldCode, fieldType, fieldClass, value) { return { fieldLocator: { fieldScope: { crypto: scope.crypto, machine: scope.machine }, code: fieldCode, fieldType, fieldClass }, fieldValue: {fieldType, value} } } function configDeleteField (scope, fieldCode) { return { fieldLocator: { fieldScope: { crypto: scope.crypto, machine: scope.machine }, code: fieldCode }, fieldValue: null } } function populateScopes (schema) { const scopeLookup = {} _.forEach(r => { const scope = { cryptoScope: r.cryptoScope, machineScope: r.machineScope } _.forEach(field => { scopeLookup[field] = scope }, r.fields) }, schema.groups) return _.map(r => _.assign(scopeLookup[r.code], r), schema.fields) } function cryptoDefaultOverride (cryptoCode, code, defaultValue) { if (cryptoCode === 'ETH' && code === 'zeroConf') { return 'no-zero-conf' } return defaultValue } function cryptoCodeDefaults (schema, cryptoCode) { const scope = {crypto: cryptoCode, machine: 'global'} const schemaEntries = populateScopes(schema) const hasCryptoSpecificDefault = r => r.cryptoScope === 'specific' && !_.isNil(r.default) const cryptoSpecificFields = _.filter(hasCryptoSpecificDefault, schemaEntries) return _.map(r => { const defaultValue = cryptoDefaultOverride(cryptoCode, r.code, r.default) return configAddField(scope, r.code, r.fieldType, r.fieldClass, defaultValue) }, cryptoSpecificFields) } const uniqCompact = _.flow(_.compact, _.uniq) function addCryptoDefaults (oldConfig, newFields) { const cryptoCodeEntries = _.filter(v => v.fieldLocator.code === 'cryptoCurrencies', newFields) const cryptoCodes = _.flatMap(_.get('fieldValue.value'), cryptoCodeEntries) const uniqueCryptoCodes = uniqCompact(cryptoCodes) const mapDefaults = cryptoCode => cryptoCodeDefaults(schema, cryptoCode) const defaults = _.flatMap(mapDefaults, uniqueCryptoCodes) return mergeValues(defaults, oldConfig) } function modifyConfig (newFields) { const TransactionMode = pgp.txMode.TransactionMode const isolationLevel = pgp.txMode.isolationLevel const mode = new TransactionMode({ tiLevel: isolationLevel.serializable }) function transaction (t) { return loadRecentConfig() .then(oldConfig => { const oldConfigWithDefaults = addCryptoDefaults(oldConfig, newFields) const doSave = _.flow(mergeValues, save) return doSave(oldConfigWithDefaults, newFields) }) } return db.tx({ mode }, transaction) } module.exports = { settings, loadConfig, loadRecentConfig, load, loadLatest, save, loadFixture, mergeValues, modifyConfig, configAddField, configDeleteField }