Merge pull request #1814 from RafaelTaranto/feat/add-unpaired-machine-names
LAM-1362 feat: add unpaired machine names
This commit is contained in:
commit
59da215788
21 changed files with 109 additions and 2381 deletions
|
|
@ -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)))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -4,30 +4,29 @@ const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
|||
|
||||
function transaction () {
|
||||
const sql = `SELECT DISTINCT * FROM (
|
||||
SELECT 'type' AS type, 'Cash In' AS value UNION
|
||||
SELECT 'type' AS type, 'Cash Out' AS value UNION
|
||||
SELECT 'machine' AS type, name AS value FROM devices d INNER JOIN cash_in_txs t ON d.device_id = t.device_id UNION
|
||||
SELECT 'machine' AS type, name AS value FROM devices d INNER JOIN cash_out_txs t ON d.device_id = t.device_id UNION
|
||||
SELECT 'customer' AS type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
|
||||
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
|
||||
SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION
|
||||
SELECT 'machine' AS type, name AS label, d.device_id AS value FROM devices d INNER JOIN cash_in_txs t ON d.device_id = t.device_id UNION
|
||||
SELECT 'machine' AS type, name AS label, d.device_id AS value FROM devices d INNER JOIN cash_out_txs t ON d.device_id = t.device_id UNION
|
||||
SELECT 'customer' AS type, NULL AS label, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
|
||||
FROM customers c INNER JOIN cash_in_txs t ON c.id = t.customer_id
|
||||
WHERE c.id_card_data::json->>'firstName' IS NOT NULL or c.id_card_data::json->>'lastName' IS NOT NULL UNION
|
||||
SELECT 'customer' AS type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
|
||||
SELECT 'customer' AS type, NULL AS label, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
|
||||
FROM customers c INNER JOIN cash_out_txs t ON c.id = t.customer_id
|
||||
WHERE c.id_card_data::json->>'firstName' IS NOT NULL or c.id_card_data::json->>'lastName' IS NOT NULL UNION
|
||||
SELECT 'fiat' AS type, fiat_code AS value FROM cash_in_txs UNION
|
||||
SELECT 'fiat' AS type, fiat_code AS value FROM cash_out_txs UNION
|
||||
SELECT 'crypto' AS type, crypto_code AS value FROM cash_in_txs UNION
|
||||
SELECT 'crypto' AS type, crypto_code AS value FROM cash_out_txs UNION
|
||||
SELECT 'address' AS type, to_address AS value FROM cash_in_txs UNION
|
||||
SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION
|
||||
SELECT 'status' AS type, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
|
||||
SELECT 'status' AS type, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs UNION
|
||||
SELECT 'sweep status' AS type, CASE WHEN swept THEN 'Swept' WHEN NOT swept THEN 'Unswept' END AS value FROM cash_out_txs
|
||||
SELECT 'fiat' AS type, NULL AS label, fiat_code AS value FROM cash_in_txs UNION
|
||||
SELECT 'fiat' AS type, NULL AS label, fiat_code AS value FROM cash_out_txs UNION
|
||||
SELECT 'crypto' AS type, NULL AS label, crypto_code AS value FROM cash_in_txs UNION
|
||||
SELECT 'crypto' AS type, NULL AS label, crypto_code AS value FROM cash_out_txs UNION
|
||||
SELECT 'address' AS type, NULL AS label, to_address AS value FROM cash_in_txs UNION
|
||||
SELECT 'address' AS type, NULL AS label, to_address AS value FROM cash_out_txs UNION
|
||||
SELECT 'status' AS type, NULL AS label, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
|
||||
SELECT 'status' AS type, NULL AS label, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs UNION
|
||||
SELECT 'sweep status' AS type, NULL AS label, CASE WHEN swept THEN 'Swept' WHEN NOT swept THEN 'Unswept' END AS value FROM cash_out_txs
|
||||
) f`
|
||||
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
function customer () {
|
||||
const sql = `SELECT DISTINCT * FROM (
|
||||
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ const resolvers = {
|
|||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||
},
|
||||
Query: {
|
||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
|
||||
transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
||||
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
|
||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTx(id, txClass).then(data =>
|
||||
|
|
|
|||
|
|
@ -56,11 +56,12 @@ const typeDef = gql`
|
|||
type Filter {
|
||||
type: String
|
||||
value: String
|
||||
label: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
transactionFilters: [Filter] @auth
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const { AuthenticationError } = require('apollo-server-express')
|
||||
const base64 = require('base-64')
|
||||
const users = require('../../users')
|
||||
|
||||
const buildApolloContext = async ({ req, res }) => {
|
||||
|
|
|
|||
|
|
@ -11,19 +11,6 @@ const { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/
|
|||
|
||||
const NUM_RESULTS = 1000
|
||||
|
||||
function addNames (txs) {
|
||||
return machineLoader.getMachineNames()
|
||||
.then(machines => {
|
||||
const addName = tx => {
|
||||
const machine = _.find(['deviceId', tx.deviceId], machines)
|
||||
const name = machine ? machine.name : 'Unpaired'
|
||||
return _.set('machineName', name, tx)
|
||||
}
|
||||
|
||||
return _.map(addName, txs)
|
||||
})
|
||||
}
|
||||
|
||||
function addProfits (txs) {
|
||||
return _.map(it => {
|
||||
const profit = getProfit(it).toString()
|
||||
|
|
@ -33,14 +20,31 @@ function addProfits (txs) {
|
|||
|
||||
const camelize = _.mapKeys(_.camelCase)
|
||||
|
||||
const DEVICE_NAME_QUERY = `
|
||||
CASE
|
||||
WHEN ud.name IS NOT NULL THEN ud.name || ' (unpaired)'
|
||||
WHEN d.name IS NOT NULL THEN d.name
|
||||
ELSE 'Unpaired'
|
||||
END AS machine_name
|
||||
`
|
||||
|
||||
const DEVICE_NAME_JOINS = `
|
||||
LEFT JOIN devices d ON txs.device_id = d.device_id
|
||||
LEFT JOIN (
|
||||
SELECT device_id, name, unpaired, paired
|
||||
FROM unpaired_devices
|
||||
) ud ON txs.device_id = ud.device_id
|
||||
AND ud.unpaired >= txs.created
|
||||
AND (txs.created >= ud.paired)
|
||||
`
|
||||
|
||||
function batch (
|
||||
from = new Date(0).toISOString(),
|
||||
until = new Date().toISOString(),
|
||||
limit = null,
|
||||
offset = 0,
|
||||
id = null,
|
||||
txClass = null,
|
||||
machineName = null,
|
||||
deviceId = null,
|
||||
customerName = null,
|
||||
fiatCode = null,
|
||||
cryptoCode = null,
|
||||
|
|
@ -61,8 +65,7 @@ function batch (
|
|||
k
|
||||
)
|
||||
)),
|
||||
addProfits,
|
||||
addNames
|
||||
addProfits
|
||||
)
|
||||
|
||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||
|
|
@ -77,21 +80,20 @@ function batch (
|
|||
txs.tx_customer_photo_at AS tx_customer_photo_at,
|
||||
txs.tx_customer_photo_path AS tx_customer_photo_path,
|
||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
|
||||
tb.error_message AS batch_error
|
||||
tb.error_message AS batch_error,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
LEFT JOIN devices d ON txs.device_id = d.device_id
|
||||
${DEVICE_NAME_JOINS}
|
||||
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
||||
id !== null ? `AND txs.device_id = $6` : ``
|
||||
}
|
||||
AND ($7 is null or $7 = 'Cash In')
|
||||
AND ($8 is null or d.name = $8)
|
||||
AND ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||
AND ($10 is null or txs.fiat_code = $10)
|
||||
AND ($11 is null or txs.crypto_code = $11)
|
||||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
WHERE txs.created >= $2 AND txs.created <= $3
|
||||
AND ($6 is null or $6 = 'Cash In')
|
||||
AND ($7 is null or txs.device_id = $7)
|
||||
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
|
||||
AND ($9 is null or txs.fiat_code = $9)
|
||||
AND ($10 is null or txs.crypto_code = $10)
|
||||
AND ($11 is null or txs.to_address = $11)
|
||||
AND ($12 is null or txs.txStatus = $12)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'}
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
|
@ -109,23 +111,22 @@ function batch (
|
|||
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||
txs.tx_customer_photo_at AS tx_customer_photo_at,
|
||||
txs.tx_customer_photo_path AS tx_customer_photo_path,
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $1) AS expired
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $1) AS expired,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs
|
||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||
AND actions.action = 'provisionAddress'
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
LEFT JOIN devices d ON txs.device_id = d.device_id
|
||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
||||
id !== null ? `AND txs.device_id = $6` : ``
|
||||
}
|
||||
AND ($7 is null or $7 = 'Cash Out')
|
||||
AND ($8 is null or d.name = $8)
|
||||
AND ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||
AND ($10 is null or txs.fiat_code = $10)
|
||||
AND ($11 is null or txs.crypto_code = $11)
|
||||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
AND ($14 is null or txs.swept = $14)
|
||||
${DEVICE_NAME_JOINS}
|
||||
WHERE txs.created >= $2 AND txs.created <= $3
|
||||
AND ($6 is null or $6 = 'Cash Out')
|
||||
AND ($7 is null or txs.device_id = $7)
|
||||
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
|
||||
AND ($9 is null or txs.fiat_code = $9)
|
||||
AND ($10 is null or txs.crypto_code = $10)
|
||||
AND ($11 is null or txs.to_address = $11)
|
||||
AND ($12 is null or txs.txStatus = $12)
|
||||
AND ($13 is null or txs.swept = $13)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
${isCsvExport ? '' : 'AND fiat > 0'}
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
|
@ -141,13 +142,13 @@ function batch (
|
|||
}
|
||||
|
||||
if (hasCashInOnlyFilters) {
|
||||
promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])]
|
||||
promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status])]
|
||||
} else if (hasCashOutOnlyFilters) {
|
||||
promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])]
|
||||
promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept])]
|
||||
} else {
|
||||
promises = [
|
||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])
|
||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status]),
|
||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept])
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ const getStatus = it => {
|
|||
function getCustomerTransactionsBatch (ids) {
|
||||
const packager = _.flow(it => {
|
||||
return it
|
||||
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize))
|
||||
|
||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||
c.phone AS customer_phone,
|
||||
|
|
@ -261,9 +262,11 @@ function getCustomerTransactionsBatch (ids) {
|
|||
c.front_camera_path AS customer_front_camera_path,
|
||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $2)) AS expired,
|
||||
tb.error_message AS batch_error
|
||||
tb.error_message AS batch_error,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM cash_in_txs AS txs
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
${DEVICE_NAME_JOINS}
|
||||
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||
WHERE c.id IN ($1^)
|
||||
ORDER BY created DESC limit $3`
|
||||
|
|
@ -279,11 +282,13 @@ function getCustomerTransactionsBatch (ids) {
|
|||
c.name AS customer_name,
|
||||
c.front_camera_path AS customer_front_camera_path,
|
||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $3) AS expired
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $3) AS expired,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM cash_out_txs txs
|
||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||
AND actions.action = 'provisionAddress'
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
${DEVICE_NAME_JOINS}
|
||||
WHERE c.id IN ($1^)
|
||||
ORDER BY created DESC limit $2`
|
||||
return Promise.all([
|
||||
|
|
@ -297,7 +302,7 @@ function getCustomerTransactionsBatch (ids) {
|
|||
}
|
||||
|
||||
function single (txId) {
|
||||
const packager = _.flow(_.compact, _.map(camelize), addNames)
|
||||
const packager = _.flow(_.compact, _.map(camelize))
|
||||
|
||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||
c.phone AS customer_phone,
|
||||
|
|
@ -309,9 +314,11 @@ function single (txId) {
|
|||
c.front_camera_path AS customer_front_camera_path,
|
||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
|
||||
tb.error_message AS batch_error
|
||||
tb.error_message AS batch_error,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM cash_in_txs AS txs
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
${DEVICE_NAME_JOINS}
|
||||
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||
WHERE id=$2`
|
||||
|
||||
|
|
@ -325,13 +332,14 @@ function single (txId) {
|
|||
c.id_card_data AS customer_id_card_data,
|
||||
c.name AS customer_name,
|
||||
c.front_camera_path AS customer_front_camera_path,
|
||||
|
||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $2) AS expired
|
||||
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $2) AS expired,
|
||||
${DEVICE_NAME_QUERY}
|
||||
FROM cash_out_txs txs
|
||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||
AND actions.action = 'provisionAddress'
|
||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||
${DEVICE_NAME_JOINS}
|
||||
WHERE id=$1`
|
||||
|
||||
return Promise.all([
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
]
|
||||
const sql = [
|
||||
'alter table devices add column name text',
|
||||
'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)
|
||||
})
|
||||
return db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ const SearchBox = memo(
|
|||
classes={{ option: classes.autocomplete }}
|
||||
value={filters}
|
||||
options={options}
|
||||
getOptionLabel={it => it.value}
|
||||
getOptionLabel={it => it.label || it.value}
|
||||
renderOption={it => (
|
||||
<div className={classes.item}>
|
||||
<P className={classes.itemLabel}>{it.value}</P>
|
||||
<P className={classes.itemLabel}>{it.label || it.value}</P>
|
||||
<P className={classes.itemType}>{it.type}</P>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const SearchFilter = ({
|
|||
<Chip
|
||||
key={idx}
|
||||
classes={chipClasses}
|
||||
label={`${onlyFirstToUpper(f.type)}: ${f.value}`}
|
||||
label={`${onlyFirstToUpper(f.type)}: ${f.label || f.value}`}
|
||||
onDelete={() => onFilterDelete(f)}
|
||||
deleteIcon={<CloseIcon className={classes.button} />}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ const MACHINE_LOGS = gql`
|
|||
`
|
||||
|
||||
const createCsv = async ({ machineLogsCsv }) => {
|
||||
console.log(machineLogsCsv)
|
||||
const machineLogs = new Blob([machineLogsCsv], {
|
||||
type: 'text/plain;charset=utf-8'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const useStyles = makeStyles(mainStyles)
|
|||
const NUM_LOG_RESULTS = 5
|
||||
|
||||
const GET_TRANSACTIONS = gql`
|
||||
query transactions($limit: Int, $from: Date, $until: Date, $deviceId: ID) {
|
||||
query transactions($limit: Int, $from: Date, $until: Date, $deviceId: String) {
|
||||
transactions(
|
||||
limit: $limit
|
||||
from: $from
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const GET_TRANSACTION_FILTERS = gql`
|
|||
transactionFilters {
|
||||
type
|
||||
value
|
||||
label
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -71,7 +72,7 @@ const GET_TRANSACTIONS = gql`
|
|||
$from: Date
|
||||
$until: Date
|
||||
$txClass: String
|
||||
$machineName: String
|
||||
$deviceId: String
|
||||
$customerName: String
|
||||
$fiatCode: String
|
||||
$cryptoCode: String
|
||||
|
|
@ -84,7 +85,7 @@ const GET_TRANSACTIONS = gql`
|
|||
from: $from
|
||||
until: $until
|
||||
txClass: $txClass
|
||||
machineName: $machineName
|
||||
deviceId: $deviceId
|
||||
customerName: $customerName
|
||||
fiatCode: $fiatCode
|
||||
cryptoCode: $cryptoCode
|
||||
|
|
@ -265,13 +266,13 @@ const Transactions = () => {
|
|||
setVariables({
|
||||
limit: NUM_LOG_RESULTS,
|
||||
txClass: filtersObject.type,
|
||||
machineName: filtersObject.machine,
|
||||
deviceId: filtersObject.machine,
|
||||
customerName: filtersObject.customer,
|
||||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
swept: filtersObject.swept && filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
@ -289,13 +290,13 @@ const Transactions = () => {
|
|||
setVariables({
|
||||
limit: NUM_LOG_RESULTS,
|
||||
txClass: filtersObject.type,
|
||||
machineName: filtersObject.machine,
|
||||
deviceId: filtersObject.machine,
|
||||
customerName: filtersObject.customer,
|
||||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
swept: filtersObject.swept && filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
@ -308,13 +309,13 @@ const Transactions = () => {
|
|||
setVariables({
|
||||
limit: NUM_LOG_RESULTS,
|
||||
txClass: filtersObject.type,
|
||||
machineName: filtersObject.machine,
|
||||
deviceId: filtersObject.machine,
|
||||
customerName: filtersObject.customer,
|
||||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
swept: filtersObject.swept && filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue