chore: deprecate old migrations

This commit is contained in:
Rafael Taranto 2025-03-27 12:22:46 +00:00
parent f5ba3dbf4e
commit ca68fdd0a2
13 changed files with 15 additions and 2293 deletions

View file

@ -1,67 +0,0 @@
const _ = require('lodash/fp')
module.exports = {
unscoped,
cryptoScoped,
machineScoped,
scoped,
scopedValue,
all
}
function matchesValue (crypto, machine, instance) {
return instance.fieldLocator.fieldScope.crypto === crypto &&
instance.fieldLocator.fieldScope.machine === machine
}
function permutations (crypto, machine) {
return _.uniq([
[crypto, machine],
[crypto, 'global'],
['global', machine],
['global', 'global']
])
}
function fallbackValue (crypto, machine, instances) {
const notNil = _.negate(_.isNil)
const pickValue = arr => _.find(instance => matchesValue(arr[0], arr[1], instance), instances)
const fallbackRec = _.find(notNil, _.map(pickValue, permutations(crypto, machine)))
return fallbackRec && fallbackRec.fieldValue.value
}
function scopedValue (crypto, machine, fieldCode, config) {
const allScopes = config.filter(_.pathEq(['fieldLocator', 'code'], fieldCode))
return fallbackValue(crypto, machine, allScopes)
}
function generalScoped (crypto, machine, config) {
const localScopedValue = key =>
scopedValue(crypto, machine, key, config)
const keys = _.uniq(_.map(r => r.fieldLocator.code, config))
const keyedValues = keys.map(localScopedValue)
return _.zipObject(keys, keyedValues)
}
function machineScoped (machine, config) {
return generalScoped('global', machine, config)
}
function unscoped (config) {
return generalScoped('global', 'global', config)
}
function cryptoScoped (crypto, config) {
return generalScoped(crypto, 'global', 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)))
}

View file

@ -1,191 +0,0 @@
const _ = require('lodash/fp')
const db = require('../db')
const configManager = require('./config-manager')
const logger = require('../logger')
const schema = require('./lamassu-schema.json')
const REMOVED_FIELDS = ['crossRefVerificationActive', 'crossRefVerificationThreshold']
const SETTINGS_LOADER_SCHEMA_VERSION = 1
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, anyFields, allFields) {
const fieldCode = field.code
const scopes = allScopes(
allCryptoScopes(cryptos, field.cryptoScope),
allMachineScopes(machineList, field.machineScope)
)
return scopes.every(scope => {
const isAnyEnabled = () => _.some(refField => {
return isScopeEnabled(config, cryptos, machineList, refField, scope)
}, anyFields)
const areAllEnabled = () => _.every(refField => {
return isScopeEnabled(config, cryptos, machineList, refField, scope)
}, allFields)
const isBlank = _.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config))
const isRequired = (_.isEmpty(anyFields) || isAnyEnabled()) &&
(_.isEmpty(allFields) || areAllEnabled())
const hasDefault = !_.isNil(_.get('default', field))
const isValid = !isRequired || !isBlank || hasDefault
return isValid
})
}
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 (fieldCode) {
const group = getGroup(fieldCode)
return getGroupField(group, fieldCode)
}
function getGroupField (group, fieldCode) {
const field = _.find(_.matchesProperty('code', fieldCode), schema.fields)
return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field)
}
// Note: We can't use machine-loader because it relies on settings-loader,
// which relies on this
function getMachines () {
return db.any('select device_id from devices')
}
function fetchMachines () {
return getMachines()
.then(machineList => machineList.map(r => r.device_id))
}
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
if (_.includes(fieldCode, REMOVED_FIELDS)) return
const field = pickField(fieldCode)
if (!field) {
logger.warn('No such field: %s, %j', fieldCode, fieldInstance.fieldLocator.fieldScope)
return
}
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 = getGroupField(group, fieldCode)
if (!field.fieldValidation.find(r => r.code === 'required')) return false
const refFieldsAny = _.map(_.partial(getField, group), field.enabledIfAny)
const refFieldsAll = _.map(_.partial(getField, group), field.enabledIfAll)
const isInvalid = !satisfiesRequire(config, cryptos, machineList, field, refFieldsAny, refFieldsAll)
return isInvalid
})
})
})
.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:' + arr)
})
}
module.exports = {
SETTINGS_LOADER_SCHEMA_VERSION,
validate,
ensureConstraints,
validateRequires
}

View file

@ -1,230 +0,0 @@
const _ = require('lodash/fp')
const devMode = require('minimist')(process.argv.slice(2)).dev
const currencies = require('../new-admin/config/data/currencies.json')
const languageRec = require('../new-admin/config/data/languages.json')
const countries = require('../new-admin/config/data/countries.json')
const machineLoader = require('../machine-loader')
const configManager = require('./config-manager')
const db = require('../db')
const settingsLoader = require('./settings-loader')
const configValidate = require('./config-validate')
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 !== '' && 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
}

File diff suppressed because it is too large Load diff

View file

@ -1,250 +0,0 @@
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.oneOrNone(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,
loadLatestConfig,
save,
loadFixture,
mergeValues,
modifyConfig,
configAddField,
configDeleteField
}

View file

@ -1,477 +0,0 @@
const _ = require('lodash/fp')
const uuid = require('uuid')
const { COINS } = require('@lamassu/coins')
const { scopedValue } = require('./admin/config-manager')
const GLOBAL = 'global'
const ALL_CRYPTOS = _.values(COINS).sort()
const ALL_CRYPTOS_STRING = 'ALL_COINS'
const ALL_MACHINES = 'ALL_MACHINES'
const GLOBAL_SCOPE = {
crypto: ALL_CRYPTOS,
machine: GLOBAL
}
function getConfigFields (codes, config) {
const stringfiedGlobalScope = JSON.stringify(GLOBAL_SCOPE)
const fields = config
.filter(i => codes.includes(i.fieldLocator.code))
.map(f => {
const crypto = Array.isArray(f.fieldLocator.fieldScope.crypto)
? f.fieldLocator.fieldScope.crypto.sort()
: f.fieldLocator.fieldScope.crypto === GLOBAL
? ALL_CRYPTOS
: [f.fieldLocator.fieldScope.crypto]
const machine = f.fieldLocator.fieldScope.machine
return {
code: f.fieldLocator.code,
scope: {
crypto,
machine
},
value: f.fieldValue.value
}
})
.filter(f => f.value != null)
const grouped = _.chain(fields)
.groupBy(f => JSON.stringify(f.scope))
.value()
return {
global: grouped[stringfiedGlobalScope] || [],
scoped:
_.entries(
_.chain(grouped)
.omit([stringfiedGlobalScope])
.value()
).map(f => {
const fallbackValues =
_.difference(codes, f[1].map(v => v.code))
.map(v => ({
code: v,
scope: JSON.parse(f[0]),
value: scopedValue(f[0].crypto, f[0].machine, v, config)
}))
.filter(f => f.value != null)
return {
scope: JSON.parse(f[0]),
values: f[1].concat(fallbackValues)
}
}) || []
}
}
function migrateCommissions (config) {
const areArraysEquals = (arr1, arr2) => Array.isArray(arr1) && Array.isArray(arr2) && _.isEmpty(_.xor(arr1, arr2))
const getMachine = _.get('scope.machine')
const getCrypto = _.get('scope.crypto')
const flattenCoins = _.compose(_.flatten, _.map(getCrypto))
const diffAllCryptos = _.compose(_.difference(ALL_CRYPTOS))
const codes = {
minimumTx: 'minimumTx',
cashInFee: 'fixedFee',
cashInCommission: 'cashIn',
cashOutCommission: 'cashOut'
}
const { global, scoped } = getConfigFields(_.keys(codes), config)
const defaultCashOutCommissions = { code: 'cashOutCommission', value: 0, scope: global[0].scope }
const isCashOutDisabled =
_.isEmpty(_.filter(commissionElement => commissionElement.code === 'cashOutCommission', global))
const globalWithDefaults =
isCashOutDisabled ? _.concat(global, defaultCashOutCommissions) : global
const machineAndCryptoScoped = scoped.filter(
f => f.scope.machine !== GLOBAL_SCOPE.machine && f.scope.crypto.length === 1
)
const cryptoScoped = scoped.filter(
f =>
f.scope.machine === GLOBAL_SCOPE.machine &&
!areArraysEquals(f.scope.crypto, GLOBAL_SCOPE.crypto)
)
const machineScoped = scoped.filter(
f =>
f.scope.machine !== GLOBAL_SCOPE.machine &&
areArraysEquals(f.scope.crypto, GLOBAL_SCOPE.crypto)
)
const withCryptoScoped = machineAndCryptoScoped.concat(cryptoScoped)
const filteredMachineScoped = _.map(it => {
const filterByMachine = _.filter(_.includes(getMachine(it)))
const unrepeatedCryptos = _.compose(
diffAllCryptos,
flattenCoins,
filterByMachine
)(withCryptoScoped)
return _.set('scope.crypto', unrepeatedCryptos)(it)
})(machineScoped)
const allCommissionsOverrides = withCryptoScoped.concat(filteredMachineScoped)
return {
..._.fromPairs(globalWithDefaults.map(f => [`commissions_${codes[f.code]}`, f.value])),
...(allCommissionsOverrides.length > 0 && {
commissions_overrides: allCommissionsOverrides.map(s => ({
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
machine: s.scope.machine === GLOBAL ? ALL_MACHINES : s.scope.machine,
cryptoCurrencies: areArraysEquals(s.scope.crypto, ALL_CRYPTOS) ? [ALL_CRYPTOS_STRING] : s.scope.crypto,
id: uuid.v4()
}))
})
}
}
function migrateLocales (config) {
const codes = {
country: 'country',
fiatCurrency: 'fiatCurrency',
machineLanguages: 'languages',
cryptoCurrencies: 'cryptoCurrencies',
timezone: 'timezone'
}
const { global, scoped } = getConfigFields(_.keys(codes), config)
return {
..._.fromPairs(global.map(f => [`locale_${codes[f.code]}`, f.value])),
...(scoped.length > 0 && {
locale_overrides: scoped.map(s => ({
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
machine: s.scope.machine,
id: uuid.v4()
}))
})
}
}
function migrateCashOut (config) {
const globalCodes = {
fudgeFactorActive: 'fudgeFactorActive'
}
const scopedCodes = {
cashOutEnabled: 'active',
topCashOutDenomination: 'top',
bottomCashOutDenomination: 'bottom',
zeroConfLimit: 'zeroConfLimit'
}
const { global } = getConfigFields(_.keys(globalCodes), config)
const { scoped } = getConfigFields(_.keys(scopedCodes), config)
return {
..._.fromPairs(
global.map(f => [`cashOut_${globalCodes[f.code]}`, f.value])
),
..._.fromPairs(
_.flatten(
scoped.map(s => {
const fields = s.values.map(f => [
`cashOut_${f.scope.machine}_${scopedCodes[f.code]}`,
f.value
])
fields.push([`cashOut_${s.scope.machine}_id`, s.scope.machine])
return fields
})
)
)
}
}
function migrateNotifications (config) {
const globalCodes = {
notificationsEmailEnabled: 'email_active',
notificationsSMSEnabled: 'sms_active',
cashOutCassette1AlertThreshold: 'fiatBalanceCassette1',
cashOutCassette2AlertThreshold: 'fiatBalanceCassette2',
cryptoAlertThreshold: 'cryptoLowBalance'
}
const machineScopedCodes = {
cashOutCassette1AlertThreshold: 'cassette1',
cashOutCassette2AlertThreshold: 'cassette2'
}
const cryptoScopedCodes = {
cryptoAlertThreshold: 'lowBalance'
}
const { global } = getConfigFields(_.keys(globalCodes), config)
const machineScoped = getConfigFields(
_.keys(machineScopedCodes),
config
).scoped.filter(f => f.scope.crypto === GLOBAL && f.scope.machine !== GLOBAL)
const cryptoScoped = getConfigFields(
_.keys(cryptoScopedCodes),
config
).scoped.filter(f => f.scope.crypto !== GLOBAL && f.scope.machine === GLOBAL)
return {
..._.fromPairs(
global.map(f => [`notifications_${globalCodes[f.code]}`, f.value])
),
notifications_email_balance: true,
notifications_email_transactions: true,
notifications_email_compliance: true,
notifications_email_errors: true,
notifications_sms_balance: true,
notifications_sms_transactions: true,
notifications_sms_compliance: true,
notifications_sms_errors: true,
...(machineScoped.length > 0 && {
notifications_fiatBalanceOverrides: machineScoped.map(s => ({
..._.fromPairs(
s.values.map(f => [machineScopedCodes[f.code], f.value])
),
machine: s.scope.machine,
id: uuid.v4()
}))
}),
...(cryptoScoped.length > 0 && {
notifications_cryptoBalanceOverrides: cryptoScoped.map(s => ({
..._.fromPairs(s.values.map(f => [cryptoScopedCodes[f.code], f.value])),
cryptoCurrency: s.scope.crypto,
id: uuid.v4()
}))
})
}
}
function migrateWallet (config) {
const codes = {
ticker: 'ticker',
wallet: 'wallet',
exchange: 'exchange',
zeroConf: 'zeroConf'
}
const { scoped } = getConfigFields(_.keys(codes), config)
return {
...(scoped.length > 0 &&
_.fromPairs(
_.flatten(
scoped.map(s =>
s.values.map(f => [
`wallets_${f.scope.crypto}_${codes[f.code]}`,
f.value
])
)
)
))
}
}
function migrateOperatorInfo (config) {
const codes = {
operatorInfoActive: 'active',
operatorInfoEmail: 'email',
operatorInfoName: 'name',
operatorInfoPhone: 'phone',
operatorInfoWebsite: 'website',
operatorInfoCompanyNumber: 'companyNumber'
}
const { global } = getConfigFields(_.keys(codes), config)
return {
..._.fromPairs(global.map(f => [`operatorInfo_${codes[f.code]}`, f.value]))
}
}
function migrateReceiptPrinting (config) {
const codes = {
receiptPrintingActive: 'active'
}
const { global } = getConfigFields(_.keys(codes), config)
return {
..._.fromPairs(global.map(f => [`receipt_${codes[f.code]}`, f.value])),
receipt_operatorWebsite: true,
receipt_operatorEmail: true,
receipt_operatorPhone: true,
receipt_companyRegistration: true,
receipt_machineLocation: true,
receipt_customerNameOrPhoneNumber: true,
receipt_exchangeRate: true,
receipt_addressQRCode: true
}
}
function migrateCoinATMRadar (config) {
const codes = ['coinAtmRadarActive', 'coinAtmRadarShowRates']
const { global } = getConfigFields(codes, config)
const coinAtmRadar = _.fromPairs(global.map(f => [f.code, f.value]))
return {
coinAtmRadar_active: coinAtmRadar.coinAtmRadarActive,
coinAtmRadar_commissions: coinAtmRadar.coinAtmRadarShowRates,
coinAtmRadar_limitsAndVerification: coinAtmRadar.coinAtmRadarShowRates
}
}
function migrateTermsAndConditions (config) {
const codes = {
termsScreenActive: 'active',
termsScreenTitle: 'title',
termsScreenText: 'text',
termsAcceptButtonText: 'acceptButtonText',
termsCancelButtonText: 'cancelButtonText'
}
const { global } = getConfigFields(_.keys(codes), config)
return {
..._.fromPairs(
global.map(f => [`termsConditions_${codes[f.code]}`, f.value])
)
}
}
function migrateComplianceTriggers (config) {
const suspensionDays = 1
const triggerTypes = {
amount: 'txAmount',
velocity: 'txVelocity',
volume: 'txVolume',
consecutiveDays: 'consecutiveDays'
}
const requirements = {
sms: 'sms',
idData: 'idCardData',
idPhoto: 'idCardPhoto',
facePhoto: 'facephoto',
sanctions: 'sanctions',
suspend: 'suspend'
}
function createTrigger (
requirement,
threshold,
suspensionDays
) {
const triggerConfig = {
id: uuid.v4(),
direction: 'both',
threshold,
thresholdDays: 1,
triggerType: triggerTypes.volume,
requirement
}
if (!requirement === 'suspend') return triggerConfig
return _.assign(triggerConfig, { suspensionDays })
}
const codes = [
'smsVerificationActive',
'smsVerificationThreshold',
'idCardDataVerificationActive',
'idCardDataVerificationThreshold',
'idCardPhotoVerificationActive',
'idCardPhotoVerificationThreshold',
'frontCameraVerificationActive',
'frontCameraVerificationThreshold',
'sanctionsVerificationActive',
'sanctionsVerificationThreshold',
'hardLimitVerificationActive',
'hardLimitVerificationThreshold',
'rejectAddressReuseActive'
]
const global = _.fromPairs(
getConfigFields(codes, config).global.map(f => [f.code, f.value])
)
const triggers = []
if (global.smsVerificationActive && _.isNumber(global.smsVerificationThreshold)) {
triggers.push(
createTrigger(requirements.sms, global.smsVerificationThreshold)
)
}
if (global.idCardDataVerificationActive && _.isNumber(global.idCardDataVerificationThreshold)) {
triggers.push(
createTrigger(requirements.idData, global.idCardDataVerificationThreshold)
)
}
if (global.idCardPhotoVerificationActive && _.isNumber(global.idCardPhotoVerificationThreshold)) {
triggers.push(
createTrigger(requirements.idPhoto, global.idCardPhotoVerificationThreshold)
)
}
if (global.frontCameraVerificationActive && _.isNumber(global.frontCameraVerificationThreshold)) {
triggers.push(
createTrigger(requirements.facePhoto, global.frontCameraVerificationThreshold)
)
}
if (global.sanctionsVerificationActive && _.isNumber(global.sanctionsVerificationThreshold)) {
triggers.push(
createTrigger(requirements.sanctions, global.sanctionsVerificationThreshold)
)
}
if (global.hardLimitVerificationActive && _.isNumber(global.hardLimitVerificationThreshold)) {
triggers.push(
createTrigger(requirements.suspend, global.hardLimitVerificationThreshold, suspensionDays)
)
}
return {
triggers,
['compliance_rejectAddressReuse']: global.rejectAddressReuseActive
}
}
function migrateConfig (config) {
return {
...migrateCommissions(config),
...migrateLocales(config),
...migrateCashOut(config),
...migrateNotifications(config),
...migrateWallet(config),
...migrateOperatorInfo(config),
...migrateReceiptPrinting(config),
...migrateCoinATMRadar(config),
...migrateTermsAndConditions(config),
...migrateComplianceTriggers(config)
}
}
function migrateAccounts (accounts) {
const accountArray = [
'bitgo',
'bitstamp',
'blockcypher',
'infura',
'itbit',
'kraken',
'mailgun',
'twilio'
]
const services = _.keyBy('code', accounts)
const serviceFields = _.mapValues(({ fields }) => _.keyBy('code', fields))(services)
const allAccounts = _.mapValues(_.mapValues(_.get('value')))(serviceFields)
return _.pick(accountArray)(allAccounts)
}
function migrate (config, accounts) {
return {
config: migrateConfig(config),
accounts: migrateAccounts(accounts)
}
}
module.exports = { migrate }

View file

@ -129,6 +129,8 @@ function getMachineNames (config) {
.then(([rawMachines, pings, events, config, heartbeat, performance]) => {
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
console.log('machines', machines)
console.log(machines.map(addName(pings, events, config)))
return machines.map(addName(pings, events, config))
})

View file

@ -1,5 +1,4 @@
const { AuthenticationError } = require('apollo-server-express')
const base64 = require('base-64')
const users = require('../../users')
const buildApolloContext = async ({ req, res }) => {

View file

@ -1,25 +1,15 @@
const db = require('./db')
const migrateTools = require('./migrate-tools')
// This migration was updated on v10.2
// it's from before 7.5 and we update one major version at a time
// Data migration was removed, keeping only the schema update
exports.up = function (next) {
return migrateTools.migrateNames()
.then(updateSql => {
const sql = [
'alter table devices add column name text',
updateSql,
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
.catch(() => {
const sql = [
'alter table devices add column name text',
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
}
exports.down = function (next) {

View file

@ -1,34 +1,9 @@
const db = require('./db')
const machineLoader = require('../lib/machine-loader')
const { migrationSaveConfig, saveAccounts, loadLatest } = require('../lib/new-settings-loader')
const { migrate } = require('../lib/config-migration')
const _ = require('lodash/fp')
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
// This migration was actually a config update
// it's from before 7.5 and we update one major version at a time
// v10.2 is good enough to deprecate it
// file still has to exist so that the migration tool doesn't throw an error
module.exports.up = function (next) {
function migrateConfig (settings) {
const newSettings = migrate(settings.config, settings.accounts)
return Promise.all([
migrationSaveConfig(newSettings.config),
saveAccounts(newSettings.accounts)
])
.then(() => next())
}
loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
.then(settings => _.isEmpty(settings.config)
? next()
: migrateConfig(settings)
)
.catch(err => {
if (err.message === 'lamassu-server is not configured') {
return next()
}
console.log(err.message)
return next(err)
})
next()
}
module.exports.down = function (next) {

View file

@ -1,16 +0,0 @@
const pgp = require('pg-promise')()
const _ = require('lodash/fp')
const settingsLoader = require('../lib/admin/settings-loader')
const machineLoader = require('../lib/machine-loader')
module.exports = {migrateNames}
function migrateNames () {
const cs = new pgp.helpers.ColumnSet(['?device_id', 'name'], {table: 'devices'})
return settingsLoader.loadLatestConfig(false)
.then(config => machineLoader.getMachineNames(config))
.then(_.map(r => ({device_id: r.deviceId, name: r.name})))
.then(data => pgp.helpers.update(data, cs) + ' WHERE t.device_id=v.device_id')
}

View file

@ -18,7 +18,6 @@
"apollo-link-http": "^1.5.17",
"apollo-upload-client": "^13.0.0",
"axios": "0.21.1",
"base-64": "^1.0.0",
"bignumber.js": "9.0.0",
"classnames": "2.2.6",
"countries-and-timezones": "^2.4.0",

View file

@ -25,7 +25,6 @@
"apollo-server-express": "2.25.1",
"argon2": "0.28.2",
"axios": "0.21.1",
"base-64": "^1.0.0",
"base-x": "3.0.9",
"base64url": "^3.0.1",
"bchaddrjs": "^0.3.0",