Merge branch 'dev' into feat/lam-1291/stress-testing
* dev: (39 commits) chore: re-add build files fix: mailgun as default fix: backwards compatibility on cashout fixed fee chore: remove coinbase as a ticker fix: expire temporary cookies on browser close fix: optimize and normalize bills and blacklist chore: data for cypress chore: deprecate old unused tables chore: remove dependency on async local storage fix: proper datetime name chore: remove extra comment chore: update build chore: migrating to nodejs 22 feat: show unpaired device names on transactions chore: deprecate old migrations fix: update yup usage on custom info requests fix: loading svg on usdc chore: lamassu coins version bump chore: lamassu coins bump on admin fix: fee for sweeps ...
This commit is contained in:
commit
5d24f9b889
124 changed files with 17979 additions and 15339 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -18,7 +18,6 @@ jobs:
|
||||||
key: ${{ runner.os }}-buildx-updatetar
|
key: ${{ runner.os }}-buildx-updatetar
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-updatetar
|
${{ runner.os }}-buildx-updatetar
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
|
@ -34,7 +33,6 @@ jobs:
|
||||||
docker create --name extract_artifact ci_image:latest
|
docker create --name extract_artifact ci_image:latest
|
||||||
docker cp extract_artifact:/lamassu-server.tar.gz ./lamassu-server.tar.gz
|
docker cp extract_artifact:/lamassu-server.tar.gz ./lamassu-server.tar.gz
|
||||||
docker rm extract_artifact
|
docker rm extract_artifact
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
nodejs 14
|
nodejs 22
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,22 @@ const _ = require('lodash/fp')
|
||||||
require('../lib/environment-helper')
|
require('../lib/environment-helper')
|
||||||
const db = require('../lib/db')
|
const db = require('../lib/db')
|
||||||
const migrate = require('../lib/migrate')
|
const migrate = require('../lib/migrate')
|
||||||
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
|
|
||||||
|
|
||||||
const createMigration = `CREATE TABLE IF NOT EXISTS migrations (
|
const createMigration = `CREATE TABLE IF NOT EXISTS migrations (
|
||||||
id serial PRIMARY KEY,
|
id serial PRIMARY KEY,
|
||||||
data json NOT NULL
|
data json NOT NULL
|
||||||
)`
|
)`
|
||||||
|
|
||||||
const select = 'select * from migrations limit 1'
|
// no need to log the migration process
|
||||||
|
process.env.SKIP_SERVER_LOGS = true
|
||||||
|
|
||||||
const getMigrateFile = () => Promise.resolve()
|
db.none(createMigration)
|
||||||
|
.then(() => migrate.run())
|
||||||
const store = defaultStore()
|
.then(() => {
|
||||||
asyncLocalStorage.run(store, () => {
|
console.log('DB Migration succeeded.')
|
||||||
db.none(createMigration)
|
process.exit(0)
|
||||||
.then(() => Promise.all([db.oneOrNone(select), getMigrateFile()]))
|
})
|
||||||
.then(([qResult, migrateFile]) => {
|
.catch(err => {
|
||||||
process.env.SKIP_SERVER_LOGS = !(qResult && _.find(({ title }) => title === '1572524820075-server-support-logs.js', qResult.data.migrations ?? []))
|
console.error('DB Migration failed: %s', err)
|
||||||
if (!qResult && migrateFile) {
|
process.exit(1)
|
||||||
return db.none('insert into migrations (id, data) values (1, $1)', [migrateFile])
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => migrate.run())
|
|
||||||
.then(() => {
|
|
||||||
console.log('DB Migration succeeded.')
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('DB Migration failed: %s', err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
require('../lib/environment-helper')
|
require('../lib/environment-helper')
|
||||||
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
|
|
||||||
const userManagement = require('../lib/new-admin/graphql/modules/userManagement')
|
const userManagement = require('../lib/new-admin/graphql/modules/userManagement')
|
||||||
const authErrors = require('../lib/new-admin/graphql/errors/authentication')
|
const authErrors = require('../lib/new-admin/graphql/errors')
|
||||||
|
|
||||||
const name = process.argv[2]
|
const name = process.argv[2]
|
||||||
const role = process.argv[3]
|
const role = process.argv[3]
|
||||||
|
|
@ -32,23 +31,21 @@ if (role !== 'user' && role !== 'superuser') {
|
||||||
process.exit(2)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
asyncLocalStorage.run(defaultStore(), () => {
|
userManagement.createRegisterToken(name, role).then(token => {
|
||||||
userManagement.createRegisterToken(name, role).then(token => {
|
if (domain === 'localhost' && process.env.NODE_ENV !== 'production') {
|
||||||
if (domain === 'localhost' && process.env.NODE_ENV !== 'production') {
|
console.log(`https://${domain}:3001/register?t=${token.token}`)
|
||||||
console.log(`https://${domain}:3001/register?t=${token.token}`)
|
} else {
|
||||||
} else {
|
console.log(`https://${domain}/register?t=${token.token}`)
|
||||||
console.log(`https://${domain}/register?t=${token.token}`)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|
||||||
if (err instanceof authErrors.UserAlreadyExistsError){
|
if (err instanceof authErrors.UserAlreadyExistsError){
|
||||||
console.log(`A user with email ${name} already exists!`)
|
console.log(`A user with email ${name} already exists!`)
|
||||||
process.exit(2)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Error: %s', err)
|
console.log('Error: %s', err)
|
||||||
process.exit(3)
|
process.exit(3)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const adminServer = require('../lib/new-admin/graphql-dev-insecure')
|
|
||||||
|
|
||||||
adminServer.run()
|
|
||||||
|
|
@ -15,7 +15,7 @@ RUN apt-get install -y -q curl \
|
||||||
net-tools \
|
net-tools \
|
||||||
tar
|
tar
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
RUN curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
RUN apt-get install nodejs -y -q
|
RUN apt-get install nodejs -y -q
|
||||||
|
|
||||||
WORKDIR lamassu-server
|
WORKDIR lamassu-server
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
FROM alpine:3.14 AS build
|
FROM node:22-alpine AS build
|
||||||
RUN apk add --no-cache nodejs npm git curl build-base net-tools python3 postgresql-dev
|
RUN apk add --no-cache npm git curl build-base net-tools python3 postgresql-dev
|
||||||
|
|
||||||
WORKDIR lamassu-server
|
WORKDIR /lamassu-server
|
||||||
|
|
||||||
COPY ["package.json", "package-lock.json", "./"]
|
COPY ["package.json", "package-lock.json", "./"]
|
||||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||||
|
|
@ -10,8 +10,8 @@ RUN npm install --production
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.14 AS l-s-base
|
FROM node:22-alpine AS l-s-base
|
||||||
RUN apk add --no-cache nodejs npm git curl bash libpq openssl ca-certificates
|
RUN apk add --no-cache npm git curl bash libpq openssl ca-certificates
|
||||||
|
|
||||||
COPY --from=build /lamassu-server /lamassu-server
|
COPY --from=build /lamassu-server /lamassu-server
|
||||||
|
|
||||||
|
|
@ -28,6 +28,8 @@ ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
||||||
FROM node:22-alpine AS build-ui
|
FROM node:22-alpine AS build-ui
|
||||||
RUN apk add --no-cache npm git curl build-base python3
|
RUN apk add --no-cache npm git curl build-base python3
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"]
|
COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"]
|
||||||
|
|
||||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||||
|
|
@ -38,7 +40,7 @@ RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM l-s-base AS l-a-s
|
FROM l-s-base AS l-a-s
|
||||||
COPY --from=build-ui /build /lamassu-server/public
|
COPY --from=build-ui /app/build /lamassu-server/public
|
||||||
|
|
||||||
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
87
lib/app.js
87
lib/app.js
|
|
@ -1,11 +1,9 @@
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const http = require('http')
|
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
const argv = require('minimist')(process.argv.slice(2))
|
const argv = require('minimist')(process.argv.slice(2))
|
||||||
|
|
||||||
require('./environment-helper')
|
require('./environment-helper')
|
||||||
const { asyncLocalStorage, defaultStore } = require('./async-storage')
|
const { loadRoutes } = require('./routes')
|
||||||
const routes = require('./routes')
|
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const poller = require('./poller')
|
const poller = require('./poller')
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
|
|
@ -16,39 +14,35 @@ const ofacUpdate = require('./ofac/update')
|
||||||
|
|
||||||
const KEY_PATH = process.env.KEY_PATH
|
const KEY_PATH = process.env.KEY_PATH
|
||||||
const CERT_PATH = process.env.CERT_PATH
|
const CERT_PATH = process.env.CERT_PATH
|
||||||
|
const CA_PATH = process.env.CA_PATH
|
||||||
const devMode = argv.dev
|
|
||||||
|
|
||||||
const version = require('../package.json').version
|
const version = require('../package.json').version
|
||||||
logger.info('Version: %s', version)
|
logger.info('Version: %s', version)
|
||||||
|
|
||||||
function run () {
|
function run () {
|
||||||
const store = defaultStore()
|
return new Promise((resolve, reject) => {
|
||||||
return asyncLocalStorage.run(store, () => {
|
let count = 0
|
||||||
return new Promise((resolve, reject) => {
|
let handler
|
||||||
let count = 0
|
|
||||||
let handler
|
|
||||||
|
|
||||||
const errorHandler = err => {
|
const errorHandler = err => {
|
||||||
count += 1
|
count += 1
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
logger.error('[%d] Retrying in 10s...', count)
|
logger.error('[%d] Retrying in 10s...', count)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = () => {
|
const runner = () => {
|
||||||
settingsLoader.loadLatest()
|
settingsLoader.loadLatest()
|
||||||
.then(settings => {
|
.then(settings => {
|
||||||
clearInterval(handler)
|
clearInterval(handler)
|
||||||
return loadSanctions(settings)
|
return loadSanctions(settings)
|
||||||
.then(() => startServer(settings))
|
.then(startServer)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
})
|
})
|
||||||
.catch(errorHandler)
|
.catch(errorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = setInterval(runner, 10000)
|
handler = setInterval(runner, 10000)
|
||||||
runner()
|
runner()
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,30 +62,27 @@ function loadSanctions (settings) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServer (settings) {
|
async function startServer () {
|
||||||
return Promise.resolve()
|
const app = await loadRoutes()
|
||||||
.then(() => {
|
|
||||||
poller.setup(['public'])
|
|
||||||
const httpsServerOptions = {
|
|
||||||
key: fs.readFileSync(KEY_PATH),
|
|
||||||
cert: fs.readFileSync(CERT_PATH),
|
|
||||||
requestCert: true,
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = devMode
|
poller.setup()
|
||||||
? http.createServer(routes.app)
|
|
||||||
: https.createServer(httpsServerOptions, routes.app)
|
|
||||||
|
|
||||||
const port = argv.port || 3000
|
const httpsServerOptions = {
|
||||||
|
key: fs.readFileSync(KEY_PATH),
|
||||||
|
cert: fs.readFileSync(CERT_PATH),
|
||||||
|
ca: fs.readFileSync(CA_PATH),
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
|
||||||
if (devMode) logger.info('In dev mode')
|
const server = https.createServer(httpsServerOptions, app)
|
||||||
|
|
||||||
server.listen(port, () => {
|
const port = argv.port || 3000
|
||||||
logger.info('lamassu-server listening on port ' +
|
|
||||||
port + ' ' + (devMode ? '(http)' : '(https)'))
|
await new Promise((resolve) =>
|
||||||
})
|
server.listen({ port }, resolve),
|
||||||
})
|
)
|
||||||
|
logger.info(`lamassu-server listening on port ${port}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { run }
|
module.exports = { run }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
const { AsyncLocalStorage } = require('async_hooks')
|
|
||||||
const asyncLocalStorage = new AsyncLocalStorage()
|
|
||||||
|
|
||||||
const defaultStore = () => {
|
|
||||||
const store = new Map()
|
|
||||||
store.set('schema', 'public')
|
|
||||||
store.set('defaultSchema', 'ERROR_SCHEMA')
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { asyncLocalStorage, defaultStore }
|
|
||||||
|
|
@ -50,7 +50,7 @@ const BINARIES = {
|
||||||
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
|
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
|
||||||
defaultDir: 'dashcore-18.1.0/bin',
|
defaultDir: 'dashcore-18.1.0/bin',
|
||||||
url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'dashcore-21.1.1/bin'
|
dir: 'dashcore-21.1.1/bin',
|
||||||
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
|
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
function convertBigNumFields (obj) {
|
function convertBigNumFields (obj) {
|
||||||
const convert = (value, key) => {
|
const convert = (value, key) => {
|
||||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) {
|
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) {
|
||||||
|
// BACKWARDS_COMPATIBILITY 10.1
|
||||||
|
// bills before 10.2 don't have fixedFee
|
||||||
|
if (key === 'fixedFee' && !value) return new BN(0).toString()
|
||||||
return value.toString()
|
return value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function createCashboxBatch (deviceId, cashboxCount) {
|
||||||
|
|
||||||
return db.tx(t => {
|
return db.tx(t => {
|
||||||
const batchId = uuid.v4()
|
const batchId = uuid.v4()
|
||||||
const q1 = t.none(sql, [batchId, deviceId])
|
const q1 = t.one(sql, [batchId, deviceId])
|
||||||
const q2 = t.none(sql2, [batchId, deviceId])
|
const q2 = t.none(sql2, [batchId, deviceId])
|
||||||
const q3 = t.none(sql3, [batchId, deviceId])
|
const q3 = t.none(sql3, [batchId, deviceId])
|
||||||
return t.batch([q1, q2, q3])
|
return t.batch([q1, q2, q3])
|
||||||
|
|
@ -133,7 +133,7 @@ function getMachineUnbatchedBills (deviceId) {
|
||||||
function getBatchById (id) {
|
function getBatchById (id) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
||||||
FROM cashbox_batches AS cb
|
FROM cash_unit_operation AS cb
|
||||||
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
|
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
|
||||||
WHERE cb.id = $1
|
WHERE cb.id = $1
|
||||||
GROUP BY cb.id
|
GROUP BY cb.id
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,12 @@ const settings = {
|
||||||
wallets_LTC_exchange: 'mock-exchange',
|
wallets_LTC_exchange: 'mock-exchange',
|
||||||
wallets_LTC_zeroConf: 'mock-zero-conf',
|
wallets_LTC_zeroConf: 'mock-zero-conf',
|
||||||
wallets_DASH_active: true,
|
wallets_DASH_active: true,
|
||||||
wallets_DASH_ticker: 'coinbase',
|
wallets_DASH_ticker: 'binance',
|
||||||
wallets_DASH_wallet: 'mock-wallet',
|
wallets_DASH_wallet: 'mock-wallet',
|
||||||
wallets_DASH_exchange: 'mock-exchange',
|
wallets_DASH_exchange: 'mock-exchange',
|
||||||
wallets_DASH_zeroConf: 'mock-zero-conf',
|
wallets_DASH_zeroConf: 'mock-zero-conf',
|
||||||
wallets_ZEC_active: true,
|
wallets_ZEC_active: true,
|
||||||
wallets_ZEC_ticker: 'coinbase',
|
wallets_ZEC_ticker: 'binance',
|
||||||
wallets_ZEC_wallet: 'mock-wallet',
|
wallets_ZEC_wallet: 'mock-wallet',
|
||||||
wallets_ZEC_exchange: 'mock-exchange',
|
wallets_ZEC_exchange: 'mock-exchange',
|
||||||
wallets_ZEC_zeroConf: 'mock-zero-conf',
|
wallets_ZEC_zeroConf: 'mock-zero-conf',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
const { asyncLocalStorage, defaultStore } = require('./async-storage')
|
|
||||||
|
|
||||||
const computeSchema = (req, res, next) => {
|
|
||||||
const store = defaultStore()
|
|
||||||
return asyncLocalStorage.run(store, () => next())
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = computeSchema
|
|
||||||
|
|
@ -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 }
|
|
||||||
85
lib/db.js
85
lib/db.js
|
|
@ -5,81 +5,12 @@ const _ = require('lodash/fp')
|
||||||
const { PSQL_URL } = require('./constants')
|
const { PSQL_URL } = require('./constants')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const eventBus = require('./event-bus')
|
const eventBus = require('./event-bus')
|
||||||
const { asyncLocalStorage, defaultStore } = require('./async-storage')
|
|
||||||
|
|
||||||
const DATABASE_NOT_REACHABLE = 'Database not reachable.'
|
const DATABASE_NOT_REACHABLE = 'Database not reachable.'
|
||||||
|
|
||||||
const stripDefaultDbFuncs = dbCtx => {
|
|
||||||
return {
|
|
||||||
ctx: dbCtx.ctx,
|
|
||||||
query: dbCtx.$query,
|
|
||||||
result: dbCtx.$result,
|
|
||||||
many: dbCtx.$many,
|
|
||||||
oneOrNone: dbCtx.$oneOrNone,
|
|
||||||
one: dbCtx.$one,
|
|
||||||
none: dbCtx.$none,
|
|
||||||
any: dbCtx.$any,
|
|
||||||
manyOrNone: dbCtx.$manyOrNone,
|
|
||||||
tx: dbCtx.$tx,
|
|
||||||
task: dbCtx.$task,
|
|
||||||
batch: dbCtx.batch,
|
|
||||||
multi: dbCtx.$multi,
|
|
||||||
connect: dbCtx.connect
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _tx = (obj, opts, cb) => {
|
|
||||||
return obj.tx(opts, t => {
|
|
||||||
return cb(stripDefaultDbFuncs(t))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const _task = (obj, opts, cb) => {
|
|
||||||
return obj.task(opts, t => {
|
|
||||||
return cb(stripDefaultDbFuncs(t))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchema = () => {
|
|
||||||
const store = asyncLocalStorage.getStore() ?? defaultStore()
|
|
||||||
return asyncLocalStorage.run(store, () => store.get('schema'))
|
|
||||||
}
|
|
||||||
const getDefaultSchema = () => 'ERROR_SCHEMA'
|
|
||||||
|
|
||||||
const searchPathWrapper = (t, cb) => {
|
|
||||||
return t.none('SET search_path TO $1:name', [getSchema()])
|
|
||||||
.then(cb.bind(t, t))
|
|
||||||
.catch(logger.error)
|
|
||||||
.finally(() => t.none('SET search_path TO $1:name', [getDefaultSchema()]))
|
|
||||||
}
|
|
||||||
|
|
||||||
const pgp = Pgp({
|
const pgp = Pgp({
|
||||||
pgNative: true,
|
pgNative: true,
|
||||||
schema: 'ERROR_SCHEMA',
|
schema: 'public',
|
||||||
extend (obj, dbContext) {
|
|
||||||
obj.__taskEx = function (cb, throwOnError = true) {
|
|
||||||
const args = pgp.utils.taskArgs(arguments)
|
|
||||||
const schema = getSchema()
|
|
||||||
if (!schema && throwOnError) {
|
|
||||||
return Promise.reject(new Error('No schema selected, cannot complete query'))
|
|
||||||
} else if (!schema) {
|
|
||||||
return Promise.resolve('No schema selected, cannot complete query')
|
|
||||||
}
|
|
||||||
return obj.task.call(this, args.options, t => searchPathWrapper(t, cb))
|
|
||||||
}
|
|
||||||
obj.$query = (query, values, qrm, throwOnError) => obj.__taskEx(t => t.query(query, values, qrm), throwOnError)
|
|
||||||
obj.$result = (query, variables, cb, thisArg) => obj.__taskEx(t => t.result(query, variables, cb, thisArg))
|
|
||||||
obj.$many = (query, variables) => obj.__taskEx(t => t.many(query, variables))
|
|
||||||
obj.$manyOrNone = (query, variables) => obj.__taskEx(t => t.manyOrNone(query, variables))
|
|
||||||
obj.$oneOrNone = (query, variables) => obj.__taskEx(t => t.oneOrNone(query, variables))
|
|
||||||
obj.$one = (query, variables) => obj.__taskEx(t => t.one(query, variables))
|
|
||||||
obj.$none = (query, variables) => obj.__taskEx(t => t.none(query, variables))
|
|
||||||
obj.$any = (query, variables) => obj.__taskEx(t => t.any(query, variables))
|
|
||||||
obj.$multi = (query, variables) => obj.__taskEx(t => t.multi(query, variables))
|
|
||||||
// when opts is not defined "cb" occupies the "opts" spot of the arguments
|
|
||||||
obj.$tx = (opts, cb) => typeof opts === 'function' ? _tx(obj, {}, opts) : _tx(obj, opts, cb)
|
|
||||||
obj.$task = (opts, cb) => typeof opts === 'function' ? _task(obj, {}, opts) : _task(obj, opts, cb)
|
|
||||||
},
|
|
||||||
error: (err, e) => {
|
error: (err, e) => {
|
||||||
if (e.cn) logger.error(DATABASE_NOT_REACHABLE)
|
if (e.cn) logger.error(DATABASE_NOT_REACHABLE)
|
||||||
else if (e.query) {
|
else if (e.query) {
|
||||||
|
|
@ -90,7 +21,7 @@ const pgp = Pgp({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const db = stripDefaultDbFuncs(pgp(PSQL_URL))
|
const db = pgp(PSQL_URL)
|
||||||
|
|
||||||
eventBus.subscribe('log', args => {
|
eventBus.subscribe('log', args => {
|
||||||
if (process.env.SKIP_SERVER_LOGS) return
|
if (process.env.SKIP_SERVER_LOGS) return
|
||||||
|
|
@ -104,14 +35,10 @@ eventBus.subscribe('log', args => {
|
||||||
|
|
||||||
const sql = `insert into server_logs
|
const sql = `insert into server_logs
|
||||||
(id, device_id, message, log_level, meta) values ($1, $2, $3, $4, $5) returning *`
|
(id, device_id, message, log_level, meta) values ($1, $2, $3, $4, $5) returning *`
|
||||||
// need to set AsyncLocalStorage (ALS) for this function as well
|
|
||||||
// because this module is imported before ALS is set up on app.js
|
db.one(sql, [uuid.v4(), '', msgToSave, level, meta])
|
||||||
const store = defaultStore()
|
.then(_.mapKeys(_.camelCase))
|
||||||
asyncLocalStorage.run(store, () => {
|
.catch(_.noop)
|
||||||
db.one(sql, [uuid.v4(), '', msgToSave, level, meta])
|
|
||||||
.then(_.mapKeys(_.camelCase))
|
|
||||||
.catch(_.noop)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = db
|
module.exports = db
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const ph = require('./plugin-helper')
|
||||||
function sendMessage (settings, rec) {
|
function sendMessage (settings, rec) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const pluginCode = settings.config.notifications_thirdParty_email
|
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ function sendMessage (settings, rec) {
|
||||||
function sendCustomerMessage (settings, rec) {
|
function sendCustomerMessage (settings, rec) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const pluginCode = settings.config.notifications_thirdParty_email
|
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
const https = require('https')
|
const { ApolloServer } = require('@apollo/server')
|
||||||
const { ApolloServer } = require('apollo-server-express')
|
|
||||||
|
|
||||||
const devMode = !!require('minimist')(process.argv.slice(2)).dev
|
const devMode = !!require('minimist')(process.argv.slice(2)).dev
|
||||||
|
|
||||||
module.exports = new ApolloServer({
|
const context = ({ req, res }) => ({
|
||||||
|
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
|
||||||
|
deviceName: req.deviceName, /* lib/middlewares/authorize.js */
|
||||||
|
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
|
||||||
|
pid: req.query.pid,
|
||||||
|
settings: req.settings, /* lib/middlewares/populateSettings.js */
|
||||||
|
})
|
||||||
|
|
||||||
|
const graphQLServer = new ApolloServer({
|
||||||
typeDefs: require('./types'),
|
typeDefs: require('./types'),
|
||||||
resolvers: require('./resolvers'),
|
resolvers: require('./resolvers'),
|
||||||
context: ({ req, res }) => ({
|
|
||||||
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
|
|
||||||
deviceName: req.deviceName, /* lib/middlewares/authorize.js */
|
|
||||||
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
|
|
||||||
pid: req.query.pid,
|
|
||||||
settings: req.settings, /* lib/middlewares/populateSettings.js */
|
|
||||||
}),
|
|
||||||
uploads: false,
|
|
||||||
playground: false,
|
|
||||||
introspection: false,
|
introspection: false,
|
||||||
formatError: error => {
|
formatError: error => {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
debug: devMode,
|
includeStacktraceInErrorResponses: devMode,
|
||||||
logger
|
logger
|
||||||
})
|
})
|
||||||
|
|
||||||
|
module.exports = { graphQLServer, context }
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
module.exports = gql`
|
module.exports = gql`
|
||||||
type Coin {
|
type Coin {
|
||||||
cryptoCode: String!
|
cryptoCode: String!
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,18 @@ const logger = new winston.Logger({
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
rewriters: [
|
rewriters: [
|
||||||
(...[,, meta]) => meta instanceof Error ? { message: meta.message, stack: meta.stack, meta } : meta
|
(...[,, meta]) => {
|
||||||
|
if (meta.isAxiosError) {
|
||||||
|
return {
|
||||||
|
message: meta.message,
|
||||||
|
status: meta.response?.status,
|
||||||
|
data: meta.response?.data,
|
||||||
|
url: meta.config?.url,
|
||||||
|
method: meta.config?.method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta instanceof Error ? { message: meta.message, stack: meta.stack, meta } : meta
|
||||||
|
}
|
||||||
],
|
],
|
||||||
exitOnError: false
|
exitOnError: false
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const dbm = require('./postgresql_interface')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
const { ApolloError } = require('apollo-server-errors');
|
const { GraphQLError } = require('graphql');
|
||||||
const { loadLatestConfig } = require('./new-settings-loader')
|
const { loadLatestConfig } = require('./new-settings-loader')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
||||||
|
|
@ -154,7 +154,7 @@ function getMachine (machineId, config) {
|
||||||
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
|
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
|
||||||
|
|
||||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
|
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
|
||||||
if (r === null) throw new ApolloError('Resource doesn\'t exist', 'NOT_FOUND')
|
if (r === null) throw new GraphQLError('Resource doesn\'t exist', { extensions: { code: 'NOT_FOUND' } })
|
||||||
else return toMachineObject(r)
|
else return toMachineObject(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
const { asyncLocalStorage, defaultStore } = require('../async-storage')
|
|
||||||
|
|
||||||
const computeSchema = (req, res, next) => {
|
|
||||||
const store = defaultStore()
|
|
||||||
asyncLocalStorage.run(store, () => {
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = computeSchema
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
const _ = require('lodash/fp')
|
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
|
||||||
const logger = require('../logger')
|
|
||||||
|
|
||||||
const IS_STRESS_TESTING = process.env.LAMASSU_STRESS_TESTING === "YES"
|
const IS_STRESS_TESTING = process.env.LAMASSU_STRESS_TESTING === "YES"
|
||||||
|
|
||||||
function sha256 (buf) {
|
function sha256 (buf) {
|
||||||
|
|
@ -14,9 +11,8 @@ function sha256 (buf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const populateDeviceId = function (req, res, next) {
|
const populateDeviceId = function (req, res, next) {
|
||||||
let deviceId = _.isFunction(req.connection.getPeerCertificate)
|
const peerCert = req.socket.getPeerCertificate ? req.socket.getPeerCertificate() : null
|
||||||
? sha256(req.connection.getPeerCertificate()?.raw)
|
let deviceId = peerCert?.raw ? sha256(peerCert.raw) : null
|
||||||
: null
|
|
||||||
|
|
||||||
if (!deviceId && IS_STRESS_TESTING)
|
if (!deviceId && IS_STRESS_TESTING)
|
||||||
deviceId = req.headers.device_id
|
deviceId = req.headers.device_id
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,24 @@ const path = require('path')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
const serveStatic = require('serve-static')
|
const serveStatic = require('serve-static')
|
||||||
const cors = require('cors')
|
|
||||||
const helmet = require('helmet')
|
const helmet = require('helmet')
|
||||||
const nocache = require('nocache')
|
const nocache = require('nocache')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const { graphqlUploadExpress } = require('graphql-upload')
|
const { ApolloServer } = require('@apollo/server')
|
||||||
const { ApolloServer } = require('apollo-server-express')
|
const { expressMiddleware } = require('@apollo/server/express4')
|
||||||
|
const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled')
|
||||||
|
const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default')
|
||||||
|
|
||||||
|
const { mergeResolvers } = require('@graphql-tools/merge')
|
||||||
|
const { makeExecutableSchema } = require('@graphql-tools/schema')
|
||||||
|
|
||||||
require('../environment-helper')
|
require('../environment-helper')
|
||||||
const { asyncLocalStorage, defaultStore } = require('../async-storage')
|
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
const exchange = require('../exchange')
|
const exchange = require('../exchange')
|
||||||
|
|
||||||
const { AuthDirective } = require('./graphql/directives')
|
const { authDirectiveTransformer } = require('./graphql/directives')
|
||||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
const { typeDefs, resolvers } = require('./graphql/schema')
|
||||||
const findOperatorId = require('../middlewares/operatorId')
|
const findOperatorId = require('../middlewares/operatorId')
|
||||||
const computeSchema = require('../compute-schema')
|
|
||||||
const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants')
|
const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants')
|
||||||
const { session, cleanUserSessions, buildApolloContext } = require('./middlewares')
|
const { session, cleanUserSessions, buildApolloContext } = require('./middlewares')
|
||||||
|
|
||||||
|
|
@ -28,6 +30,7 @@ const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
const HOSTNAME = process.env.HOSTNAME
|
const HOSTNAME = process.env.HOSTNAME
|
||||||
const KEY_PATH = process.env.KEY_PATH
|
const KEY_PATH = process.env.KEY_PATH
|
||||||
const CERT_PATH = process.env.CERT_PATH
|
const CERT_PATH = process.env.CERT_PATH
|
||||||
|
const CA_PATH = process.env.CA_PATH
|
||||||
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
|
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
|
||||||
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
|
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
|
||||||
const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR
|
const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR
|
||||||
|
|
@ -37,74 +40,85 @@ if (!HOSTNAME) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = express()
|
const loadRoutes = async () => {
|
||||||
|
const app = express()
|
||||||
|
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
app.use(compression())
|
app.use(compression())
|
||||||
app.use(nocache())
|
app.use(nocache())
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(express.urlencoded({ extended: true })) // support encoded bodies
|
app.use(express.urlencoded({ extended: true })) // support encoded bodies
|
||||||
app.use(express.static(path.resolve(__dirname, '..', '..', 'public')))
|
app.use(express.static(path.resolve(__dirname, '..', '..', 'public')))
|
||||||
app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL))
|
app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL))
|
||||||
app.use(computeSchema)
|
app.use(findOperatorId)
|
||||||
app.use(findOperatorId)
|
app.use(session)
|
||||||
app.use(session)
|
|
||||||
app.use(graphqlUploadExpress())
|
|
||||||
|
|
||||||
const apolloServer = new ApolloServer({
|
// Dynamic import for graphql-upload since it's not a CommonJS module
|
||||||
typeDefs,
|
const { default: graphqlUploadExpress } = await import('graphql-upload/graphqlUploadExpress.mjs')
|
||||||
resolvers,
|
const { default: GraphQLUpload } = await import('graphql-upload/GraphQLUpload.mjs')
|
||||||
uploads: false,
|
|
||||||
schemaDirectives: {
|
|
||||||
auth: AuthDirective
|
|
||||||
},
|
|
||||||
playground: false,
|
|
||||||
introspection: false,
|
|
||||||
formatError: error => {
|
|
||||||
const exception = error?.extensions?.exception
|
|
||||||
logger.error(error, JSON.stringify(exception || {}))
|
|
||||||
return error
|
|
||||||
},
|
|
||||||
context: async (obj) => buildApolloContext(obj)
|
|
||||||
})
|
|
||||||
|
|
||||||
apolloServer.applyMiddleware({
|
app.use(graphqlUploadExpress())
|
||||||
app,
|
|
||||||
cors: {
|
|
||||||
credentials: true,
|
|
||||||
origin: devMode && 'https://localhost:3001'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// cors on app for /api/register endpoint.
|
const schema = makeExecutableSchema({
|
||||||
app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' }))
|
typeDefs,
|
||||||
|
resolvers: mergeResolvers(resolvers, { Upload: GraphQLUpload }),
|
||||||
|
})
|
||||||
|
const schemaWithDirectives = authDirectiveTransformer(schema)
|
||||||
|
|
||||||
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false }))
|
const apolloServer = new ApolloServer({
|
||||||
app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false }))
|
schema: schemaWithDirectives,
|
||||||
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
|
csrfPrevention: false,
|
||||||
|
introspection: false,
|
||||||
|
formatError: (formattedError, error) => {
|
||||||
|
logger.error(error, JSON.stringify(error?.extensions || {}))
|
||||||
|
return formattedError
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
devMode
|
||||||
|
? ApolloServerPluginLandingPageLocalDefault()
|
||||||
|
: ApolloServerPluginLandingPageDisabled()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
// Everything not on graphql or api/register is redirected to the front-end
|
await apolloServer.start();
|
||||||
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
|
|
||||||
|
app.use(
|
||||||
|
'/graphql',
|
||||||
|
express.json(),
|
||||||
|
expressMiddleware(apolloServer, {
|
||||||
|
context: async ({ req, res }) => buildApolloContext({ req, res })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false }))
|
||||||
|
app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false }))
|
||||||
|
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
|
||||||
|
|
||||||
|
// Everything not on graphql or api/register is redirected to the front-end
|
||||||
|
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
const certOptions = {
|
const certOptions = {
|
||||||
key: fs.readFileSync(KEY_PATH),
|
key: fs.readFileSync(KEY_PATH),
|
||||||
cert: fs.readFileSync(CERT_PATH)
|
cert: fs.readFileSync(CERT_PATH),
|
||||||
|
ca: fs.readFileSync(CA_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function run () {
|
async function run () {
|
||||||
const store = defaultStore()
|
const app = await loadRoutes()
|
||||||
asyncLocalStorage.run(store, () => {
|
const serverPort = devMode ? 8070 : 443
|
||||||
const serverPort = devMode ? 8070 : 443
|
|
||||||
|
|
||||||
const serverLog = `lamassu-admin-server listening on port ${serverPort}`
|
const serverLog = `lamassu-admin-server listening on port ${serverPort}`
|
||||||
|
|
||||||
// cache markets on startup
|
// cache markets on startup
|
||||||
exchange.getMarkets().catch(console.error)
|
exchange.getMarkets().catch(console.error)
|
||||||
|
|
||||||
const webServer = https.createServer(certOptions, app)
|
const webServer = https.createServer(certOptions, app)
|
||||||
webServer.listen(serverPort, () => logger.info(serverLog))
|
webServer.listen(serverPort, () => logger.info(serverLog))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { run }
|
module.exports = { run }
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
const { ALL } = require('../../plugins/common/ccxt')
|
const { ALL } = require('../../plugins/common/ccxt')
|
||||||
|
|
||||||
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS
|
||||||
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
|
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
|
||||||
|
|
||||||
const TICKER = 'ticker'
|
const TICKER = 'ticker'
|
||||||
const WALLET = 'wallet'
|
const WALLET = 'wallet'
|
||||||
|
|
@ -26,14 +26,13 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
|
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
|
||||||
{ code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.CRYPTO },
|
{ code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.CRYPTO },
|
||||||
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: bitstamp.CRYPTO },
|
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: bitstamp.CRYPTO },
|
||||||
{ code: 'coinbase', display: 'Coinbase', class: TICKER, cryptos: coinbase.CRYPTO },
|
|
||||||
{ code: 'itbit', display: 'itBit', class: TICKER, cryptos: itbit.CRYPTO },
|
{ code: 'itbit', display: 'itBit', class: TICKER, cryptos: itbit.CRYPTO },
|
||||||
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
||||||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||||
{ code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT] },
|
{ code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT, USDC] },
|
||||||
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
|
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
|
||||||
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT] },
|
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] },
|
||||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||||
|
|
@ -61,8 +60,8 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
||||||
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
|
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
|
||||||
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
|
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
|
||||||
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] },
|
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX] },
|
||||||
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDT_TRON, TRX, ZEC] },
|
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC] },
|
||||||
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
|
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
|
||||||
{ code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
|
{ code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
|
||||||
{ code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
|
{ code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,29 @@ const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
||||||
|
|
||||||
function transaction () {
|
function transaction () {
|
||||||
const sql = `SELECT DISTINCT * FROM (
|
const sql = `SELECT DISTINCT * FROM (
|
||||||
SELECT 'type' AS type, 'Cash In' AS value UNION
|
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
|
||||||
SELECT 'type' AS type, 'Cash Out' AS value UNION
|
SELECT 'type' AS type, NULL AS label, '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 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 value FROM devices d INNER JOIN cash_out_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, 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_in_txs t ON c.id = t.customer_id
|
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
|
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
|
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
|
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, NULL AS label, fiat_code AS value FROM cash_in_txs UNION
|
||||||
SELECT 'fiat' AS type, fiat_code AS value FROM cash_out_txs UNION
|
SELECT 'fiat' AS type, NULL AS label, 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, NULL AS label, crypto_code AS value FROM cash_in_txs UNION
|
||||||
SELECT 'crypto' AS type, crypto_code AS value FROM cash_out_txs UNION
|
SELECT 'crypto' AS type, NULL AS label, 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, NULL AS label, to_address AS value FROM cash_in_txs UNION
|
||||||
SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION
|
SELECT 'address' AS type, NULL AS label, 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, NULL AS label, ${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 'status' AS type, NULL AS label, ${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 '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`
|
) f`
|
||||||
|
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function customer () {
|
function customer () {
|
||||||
const sql = `SELECT DISTINCT * FROM (
|
const sql = `SELECT DISTINCT * FROM (
|
||||||
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
const express = require('express')
|
|
||||||
const { ApolloServer } = require('apollo-server-express')
|
|
||||||
|
|
||||||
require('../environment-helper')
|
|
||||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
|
||||||
const logger = require('../logger')
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs,
|
|
||||||
resolvers
|
|
||||||
})
|
|
||||||
|
|
||||||
server.applyMiddleware({ app })
|
|
||||||
|
|
||||||
app.use(express.json())
|
|
||||||
|
|
||||||
function run () {
|
|
||||||
const serverLog = `lamassu-admin-server listening on port ${9010}${server.graphqlPath}`
|
|
||||||
|
|
||||||
app.listen(9010, () => logger.info(serverLog))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { run }
|
|
||||||
|
|
@ -1,40 +1,49 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils')
|
||||||
const { SchemaDirectiveVisitor, AuthenticationError } = require('apollo-server-express')
|
|
||||||
const { defaultFieldResolver } = require('graphql')
|
const { defaultFieldResolver } = require('graphql')
|
||||||
|
|
||||||
class AuthDirective extends SchemaDirectiveVisitor {
|
const { AuthenticationError } = require('../errors')
|
||||||
visitObject (type) {
|
|
||||||
this.ensureFieldsWrapped(type)
|
|
||||||
type._requiredAuthRole = this.args.requires
|
|
||||||
}
|
|
||||||
|
|
||||||
visitFieldDefinition (field, details) {
|
function authDirectiveTransformer(schema, directiveName = 'auth') {
|
||||||
this.ensureFieldsWrapped(details.objectType)
|
return mapSchema(schema, {
|
||||||
field._requiredAuthRole = this.args.requires
|
// For object types
|
||||||
}
|
[MapperKind.OBJECT_TYPE]: (objectType) => {
|
||||||
|
const directive = getDirective(schema, objectType, directiveName)?.[0]
|
||||||
|
if (directive) {
|
||||||
|
const requiredAuthRole = directive.requires
|
||||||
|
objectType._requiredAuthRole = requiredAuthRole
|
||||||
|
}
|
||||||
|
return objectType
|
||||||
|
},
|
||||||
|
|
||||||
ensureFieldsWrapped (objectType) {
|
// For field definitions
|
||||||
if (objectType._authFieldsWrapped) return
|
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
|
||||||
objectType._authFieldsWrapped = true
|
const directive = getDirective(schema, fieldConfig, directiveName)?.[0]
|
||||||
|
if (directive) {
|
||||||
|
const requiredAuthRole = directive.requires
|
||||||
|
fieldConfig._requiredAuthRole = requiredAuthRole
|
||||||
|
}
|
||||||
|
|
||||||
const fields = objectType.getFields()
|
// Get the parent object type
|
||||||
|
const objectType = schema.getType(typeName)
|
||||||
|
|
||||||
_.forEach(fieldName => {
|
// Apply auth check to the field's resolver
|
||||||
const field = fields[fieldName]
|
const { resolve = defaultFieldResolver } = fieldConfig
|
||||||
const { resolve = defaultFieldResolver } = field
|
fieldConfig.resolve = function (root, args, context, info) {
|
||||||
|
const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole
|
||||||
field.resolve = function (root, args, context, info) {
|
|
||||||
const requiredRoles = field._requiredAuthRole ? field._requiredAuthRole : objectType._requiredAuthRole
|
|
||||||
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
|
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
|
||||||
|
|
||||||
const user = context.req.session.user
|
const user = context.req.session.user
|
||||||
if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) throw new AuthenticationError('You do not have permission to access this resource!')
|
if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) {
|
||||||
|
throw new AuthenticationError('You do not have permission to access this resource!')
|
||||||
|
}
|
||||||
|
|
||||||
return resolve.apply(this, [root, args, context, info])
|
return resolve.apply(this, [root, args, context, info])
|
||||||
}
|
}
|
||||||
}, _.keys(fields))
|
|
||||||
}
|
return fieldConfig
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AuthDirective
|
module.exports = authDirectiveTransformer
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
const AuthDirective = require('./auth')
|
const authDirectiveTransformer = require('./auth')
|
||||||
|
|
||||||
module.exports = { AuthDirective }
|
module.exports = { authDirectiveTransformer }
|
||||||
|
|
|
||||||
71
lib/new-admin/graphql/errors.js
Normal file
71
lib/new-admin/graphql/errors.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
const { GraphQLError } = require('graphql')
|
||||||
|
const { ApolloServerErrorCode } = require('@apollo/server/errors')
|
||||||
|
|
||||||
|
class AuthenticationError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('Authentication failed', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidCredentialsError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid credentials', {
|
||||||
|
extensions: {
|
||||||
|
code: 'INVALID_CREDENTIALS'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserAlreadyExistsError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('User already exists', {
|
||||||
|
extensions: {
|
||||||
|
code: 'USER_ALREADY_EXISTS'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidTwoFactorError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid two-factor code', {
|
||||||
|
extensions: {
|
||||||
|
code: 'INVALID_TWO_FACTOR_CODE'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidUrlError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid URL token', {
|
||||||
|
extensions: {
|
||||||
|
code: 'INVALID_URL_TOKEN'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserInputError extends GraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('User input error', {
|
||||||
|
extensions: {
|
||||||
|
code: ApolloServerErrorCode.BAD_USER_INPUT
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AuthenticationError,
|
||||||
|
InvalidCredentialsError,
|
||||||
|
UserAlreadyExistsError,
|
||||||
|
InvalidTwoFactorError,
|
||||||
|
InvalidUrlError,
|
||||||
|
UserInputError
|
||||||
|
}
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
const { ApolloError, AuthenticationError } = require('apollo-server-express')
|
|
||||||
|
|
||||||
class InvalidCredentialsError extends ApolloError {
|
|
||||||
constructor(message) {
|
|
||||||
super(message, 'INVALID_CREDENTIALS')
|
|
||||||
Object.defineProperty(this, 'name', { value: 'InvalidCredentialsError' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserAlreadyExistsError extends ApolloError {
|
|
||||||
constructor(message) {
|
|
||||||
super(message, 'USER_ALREADY_EXISTS')
|
|
||||||
Object.defineProperty(this, 'name', { value: 'UserAlreadyExistsError' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvalidTwoFactorError extends ApolloError {
|
|
||||||
constructor(message) {
|
|
||||||
super(message, 'INVALID_TWO_FACTOR_CODE')
|
|
||||||
Object.defineProperty(this, 'name', { value: 'InvalidTwoFactorError' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvalidUrlError extends ApolloError {
|
|
||||||
constructor(message) {
|
|
||||||
super(message, 'INVALID_URL_TOKEN')
|
|
||||||
Object.defineProperty(this, 'name', { value: 'InvalidUrlError' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
AuthenticationError,
|
|
||||||
InvalidCredentialsError,
|
|
||||||
UserAlreadyExistsError,
|
|
||||||
InvalidTwoFactorError,
|
|
||||||
InvalidUrlError
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@ const loginHelper = require('../../services/login')
|
||||||
const T = require('../../../time')
|
const T = require('../../../time')
|
||||||
const users = require('../../../users')
|
const users = require('../../../users')
|
||||||
const sessionManager = require('../../../session-manager')
|
const sessionManager = require('../../../session-manager')
|
||||||
const authErrors = require('../errors/authentication')
|
const authErrors = require('../errors')
|
||||||
const credentials = require('../../../hardware-credentials')
|
const credentials = require('../../../hardware-credentials')
|
||||||
|
|
||||||
const REMEMBER_ME_AGE = 90 * T.day
|
const REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
const { GraphQLDateTime } = require('graphql-iso-date')
|
const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars')
|
||||||
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
|
||||||
const { GraphQLUpload } = require('graphql-upload')
|
|
||||||
GraphQLDateTime.name = 'Date'
|
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
JSON: GraphQLJSON,
|
JSON: JSONResolver,
|
||||||
JSONObject: GraphQLJSONObject,
|
JSONObject: JSONObjectResolver,
|
||||||
Date: GraphQLDateTime,
|
DateTimeISO: DateTimeISOResolver
|
||||||
UploadGQL: GraphQLUpload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ const resolvers = {
|
||||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
||||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, 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, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
|
transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, 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.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']))),
|
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
|
||||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||||
transactions.getTx(id, txClass).then(data =>
|
transactions.getTx(id, txClass).then(data =>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Bill {
|
type Bill {
|
||||||
|
|
@ -6,7 +6,7 @@ const typeDef = gql`
|
||||||
fiat: Int
|
fiat: Int
|
||||||
fiatCode: String
|
fiatCode: String
|
||||||
deviceId: ID
|
deviceId: ID
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
cashUnitOperationId: ID
|
cashUnitOperationId: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Blacklist {
|
type Blacklist {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type CashboxBatch {
|
type CashboxBatch {
|
||||||
id: ID
|
id: ID
|
||||||
deviceId: ID
|
deviceId: ID
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
operationType: String
|
operationType: String
|
||||||
customBillCount: Int
|
customBillCount: Int
|
||||||
performedBy: String
|
performedBy: String
|
||||||
|
|
@ -14,7 +14,7 @@ const typeDef = gql`
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
cashboxBatches: [CashboxBatch] @auth
|
cashboxBatches: [CashboxBatch] @auth
|
||||||
cashboxBatchesCsv(from: Date, until: Date, timezone: String): String @auth
|
cashboxBatchesCsv(from: DateTimeISO, until: DateTimeISO, timezone: String): String @auth
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Country {
|
type Country {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Currency {
|
type Currency {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ const typeDef = gql`
|
||||||
customerId: ID
|
customerId: ID
|
||||||
infoRequestId: ID
|
infoRequestId: ID
|
||||||
override: String
|
override: String
|
||||||
overrideAt: Date
|
overrideAt: DateTimeISO
|
||||||
overrideBy: ID
|
overrideBy: ID
|
||||||
customerData: JSON
|
customerData: JSON
|
||||||
customInfoRequest: CustomInfoRequest
|
customInfoRequest: CustomInfoRequest
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Customer {
|
type Customer {
|
||||||
|
|
@ -6,10 +6,10 @@ const typeDef = gql`
|
||||||
authorizedOverride: String
|
authorizedOverride: String
|
||||||
daysSuspended: Int
|
daysSuspended: Int
|
||||||
isSuspended: Boolean
|
isSuspended: Boolean
|
||||||
newPhoto: UploadGQL
|
newPhoto: Upload
|
||||||
photoType: String
|
photoType: String
|
||||||
frontCameraPath: String
|
frontCameraPath: String
|
||||||
frontCameraAt: Date
|
frontCameraAt: DateTimeISO
|
||||||
frontCameraOverride: String
|
frontCameraOverride: String
|
||||||
phone: String
|
phone: String
|
||||||
email: String
|
email: String
|
||||||
|
|
@ -17,19 +17,19 @@ const typeDef = gql`
|
||||||
smsOverride: String
|
smsOverride: String
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
idCardDataOverride: String
|
idCardDataOverride: String
|
||||||
idCardDataExpiration: Date
|
idCardDataExpiration: DateTimeISO
|
||||||
idCardPhoto: UploadGQL
|
idCardPhoto: Upload
|
||||||
idCardPhotoPath: String
|
idCardPhotoPath: String
|
||||||
idCardPhotoOverride: String
|
idCardPhotoOverride: String
|
||||||
idCardPhotoAt: Date
|
idCardPhotoAt: DateTimeISO
|
||||||
usSsn: String
|
usSsn: String
|
||||||
usSsnOverride: String
|
usSsnOverride: String
|
||||||
sanctions: Boolean
|
sanctions: Boolean
|
||||||
sanctionsAt: Date
|
sanctionsAt: DateTimeISO
|
||||||
sanctionsOverride: String
|
sanctionsOverride: String
|
||||||
totalTxs: Int
|
totalTxs: Int
|
||||||
totalSpent: String
|
totalSpent: String
|
||||||
lastActive: Date
|
lastActive: DateTimeISO
|
||||||
lastTxFiat: String
|
lastTxFiat: String
|
||||||
lastTxFiatCode: String
|
lastTxFiatCode: String
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
|
|
@ -53,28 +53,28 @@ const typeDef = gql`
|
||||||
smsOverride: String
|
smsOverride: String
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
idCardDataOverride: String
|
idCardDataOverride: String
|
||||||
idCardDataExpiration: Date
|
idCardDataExpiration: DateTimeISO
|
||||||
idCardPhotoPath: String
|
idCardPhotoPath: String
|
||||||
idCardPhotoOverride: String
|
idCardPhotoOverride: String
|
||||||
usSsn: String
|
usSsn: String
|
||||||
usSsnOverride: String
|
usSsnOverride: String
|
||||||
sanctions: Boolean
|
sanctions: Boolean
|
||||||
sanctionsAt: Date
|
sanctionsAt: DateTimeISO
|
||||||
sanctionsOverride: String
|
sanctionsOverride: String
|
||||||
totalTxs: Int
|
totalTxs: Int
|
||||||
totalSpent: String
|
totalSpent: String
|
||||||
lastActive: Date
|
lastActive: DateTimeISO
|
||||||
lastTxFiat: String
|
lastTxFiat: String
|
||||||
lastTxFiatCode: String
|
lastTxFiatCode: String
|
||||||
lastTxClass: String
|
lastTxClass: String
|
||||||
suspendedUntil: Date
|
suspendedUntil: DateTimeISO
|
||||||
subscriberInfo: Boolean
|
subscriberInfo: Boolean
|
||||||
phoneOverride: String
|
phoneOverride: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerEdit {
|
input CustomerEdit {
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
idCardPhoto: UploadGQL
|
idCardPhoto: Upload
|
||||||
usSsn: String
|
usSsn: String
|
||||||
subscriberInfo: JSONObject
|
subscriberInfo: JSONObject
|
||||||
}
|
}
|
||||||
|
|
@ -82,8 +82,8 @@ const typeDef = gql`
|
||||||
type CustomerNote {
|
type CustomerNote {
|
||||||
id: ID
|
id: ID
|
||||||
customerId: ID
|
customerId: ID
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
lastEditedAt: Date
|
lastEditedAt: DateTimeISO
|
||||||
lastEditedBy: ID
|
lastEditedBy: ID
|
||||||
title: String
|
title: String
|
||||||
content: String
|
content: String
|
||||||
|
|
@ -108,7 +108,7 @@ const typeDef = gql`
|
||||||
removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth
|
removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth
|
||||||
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||||
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||||
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
|
replacePhoto(customerId: ID!, photoType: String, newPhoto: Upload): Customer @auth
|
||||||
createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
|
createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
|
||||||
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
|
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
|
||||||
deleteCustomerNote(noteId: ID!): Boolean @auth
|
deleteCustomerNote(noteId: ID!): Boolean @auth
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type CoinFunds {
|
type CoinFunds {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type MachineLog {
|
type MachineLog {
|
||||||
id: ID!
|
id: ID!
|
||||||
logLevel: String!
|
logLevel: String!
|
||||||
timestamp: Date!
|
timestamp: DateTimeISO!
|
||||||
message: String!
|
message: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerLog {
|
type ServerLog {
|
||||||
id: ID!
|
id: ID!
|
||||||
logLevel: String!
|
logLevel: String!
|
||||||
timestamp: Date!
|
timestamp: DateTimeISO!
|
||||||
message: String
|
message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog] @auth
|
machineLogs(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [MachineLog] @auth
|
||||||
machineLogsCsv(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int, timezone: String): String @auth
|
machineLogsCsv(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
|
||||||
serverLogs(from: Date, until: Date, limit: Int, offset: Int): [ServerLog] @auth
|
serverLogs(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [ServerLog] @auth
|
||||||
serverLogsCsv(from: Date, until: Date, limit: Int, offset: Int, timezone: String): String @auth
|
serverLogsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type IndividualDiscount {
|
type IndividualDiscount {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type MachineStatus {
|
type MachineStatus {
|
||||||
|
|
@ -10,8 +10,8 @@ const typeDef = gql`
|
||||||
name: String!
|
name: String!
|
||||||
deviceId: ID!
|
deviceId: ID!
|
||||||
paired: Boolean!
|
paired: Boolean!
|
||||||
lastPing: Date
|
lastPing: DateTimeISO
|
||||||
pairedAt: Date
|
pairedAt: DateTimeISO
|
||||||
diagnostics: Diagnostics
|
diagnostics: Diagnostics
|
||||||
version: String
|
version: String
|
||||||
model: String
|
model: String
|
||||||
|
|
@ -26,9 +26,9 @@ const typeDef = gql`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Diagnostics {
|
type Diagnostics {
|
||||||
timestamp: Date
|
timestamp: DateTimeISO
|
||||||
frontTimestamp: Date
|
frontTimestamp: DateTimeISO
|
||||||
scanTimestamp: Date
|
scanTimestamp: DateTimeISO
|
||||||
}
|
}
|
||||||
|
|
||||||
type CashUnits {
|
type CashUnits {
|
||||||
|
|
@ -64,8 +64,8 @@ const typeDef = gql`
|
||||||
deviceId: ID!
|
deviceId: ID!
|
||||||
name: String
|
name: String
|
||||||
model: String
|
model: String
|
||||||
paired: Date!
|
paired: DateTimeISO!
|
||||||
unpaired: Date!
|
unpaired: DateTimeISO!
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachineEvent {
|
type MachineEvent {
|
||||||
|
|
@ -73,9 +73,9 @@ const typeDef = gql`
|
||||||
deviceId: String
|
deviceId: String
|
||||||
eventType: String
|
eventType: String
|
||||||
note: String
|
note: String
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
age: Float
|
age: Float
|
||||||
deviceTime: Date
|
deviceTime: DateTimeISO
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MachineAction {
|
enum MachineAction {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Notification {
|
type Notification {
|
||||||
|
|
@ -6,7 +6,7 @@ const typeDef = gql`
|
||||||
type: String
|
type: String
|
||||||
detail: JSON
|
detail: JSON
|
||||||
message: String
|
message: String
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
read: Boolean
|
read: Boolean
|
||||||
valid: Boolean
|
valid: Boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Rate {
|
type Rate {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type SanctionMatches {
|
type SanctionMatches {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
scalar JSON
|
scalar JSON
|
||||||
scalar JSONObject
|
scalar JSONObject
|
||||||
scalar Date
|
scalar DateTimeISO
|
||||||
scalar UploadGQL
|
scalar Upload
|
||||||
`
|
`
|
||||||
|
|
||||||
module.exports = typeDef
|
module.exports = typeDef
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type SMSNotice {
|
type SMSNotice {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type ProcessStatus {
|
type ProcessStatus {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Transaction {
|
type Transaction {
|
||||||
|
|
@ -14,12 +14,12 @@ const typeDef = gql`
|
||||||
txHash: String
|
txHash: String
|
||||||
phone: String
|
phone: String
|
||||||
error: String
|
error: String
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
send: Boolean
|
send: Boolean
|
||||||
sendConfirmed: Boolean
|
sendConfirmed: Boolean
|
||||||
dispense: Boolean
|
dispense: Boolean
|
||||||
timedout: Boolean
|
timedout: Boolean
|
||||||
sendTime: Date
|
sendTime: DateTimeISO
|
||||||
errorCode: String
|
errorCode: String
|
||||||
operatorCompleted: Boolean
|
operatorCompleted: Boolean
|
||||||
sendPending: Boolean
|
sendPending: Boolean
|
||||||
|
|
@ -35,7 +35,7 @@ const typeDef = gql`
|
||||||
customerPhone: String
|
customerPhone: String
|
||||||
customerEmail: String
|
customerEmail: String
|
||||||
customerIdCardDataNumber: String
|
customerIdCardDataNumber: String
|
||||||
customerIdCardDataExpiration: Date
|
customerIdCardDataExpiration: DateTimeISO
|
||||||
customerIdCardData: JSONObject
|
customerIdCardData: JSONObject
|
||||||
customerName: String
|
customerName: String
|
||||||
customerFrontCameraPath: String
|
customerFrontCameraPath: String
|
||||||
|
|
@ -44,9 +44,9 @@ const typeDef = gql`
|
||||||
machineName: String
|
machineName: String
|
||||||
discount: Int
|
discount: Int
|
||||||
txCustomerPhotoPath: String
|
txCustomerPhotoPath: String
|
||||||
txCustomerPhotoAt: Date
|
txCustomerPhotoAt: DateTimeISO
|
||||||
batched: Boolean
|
batched: Boolean
|
||||||
batchTime: Date
|
batchTime: DateTimeISO
|
||||||
batchError: String
|
batchError: String
|
||||||
walletScore: Int
|
walletScore: Int
|
||||||
profit: String
|
profit: String
|
||||||
|
|
@ -56,11 +56,12 @@ const typeDef = gql`
|
||||||
type Filter {
|
type Filter {
|
||||||
type: String
|
type: String
|
||||||
value: String
|
value: String
|
||||||
|
label: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
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
|
transactions(from: DateTimeISO, until: DateTimeISO, 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, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
transactionsCsv(from: DateTimeISO, until: DateTimeISO, 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
|
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
transactionFilters: [Filter] @auth
|
transactionFilters: [Filter] @auth
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ const typeDef = `
|
||||||
type UserSession {
|
type UserSession {
|
||||||
sid: String!
|
sid: String!
|
||||||
sess: JSONObject!
|
sess: JSONObject!
|
||||||
expire: Date!
|
expire: DateTimeISO!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
|
@ -53,8 +53,8 @@ const typeDef = `
|
||||||
username: String
|
username: String
|
||||||
role: String
|
role: String
|
||||||
enabled: Boolean
|
enabled: Boolean
|
||||||
created: Date
|
created: DateTimeISO
|
||||||
last_accessed: Date
|
last_accessed: DateTimeISO
|
||||||
last_accessed_from: String
|
last_accessed_from: String
|
||||||
last_accessed_address: String
|
last_accessed_address: String
|
||||||
}
|
}
|
||||||
|
|
@ -68,14 +68,14 @@ const typeDef = `
|
||||||
type ResetToken {
|
type ResetToken {
|
||||||
token: String
|
token: String
|
||||||
user_id: ID
|
user_id: ID
|
||||||
expire: Date
|
expire: DateTimeISO
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegistrationToken {
|
type RegistrationToken {
|
||||||
token: String
|
token: String
|
||||||
username: String
|
username: String
|
||||||
role: String
|
role: String
|
||||||
expire: Date
|
expire: DateTimeISO
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const { gql } = require('apollo-server-express')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
const { asyncLocalStorage } = require('../../async-storage')
|
|
||||||
const db = require('../../db')
|
const db = require('../../db')
|
||||||
const { USER_SESSIONS_TABLE_NAME } = require('../../constants')
|
const { USER_SESSIONS_TABLE_NAME } = require('../../constants')
|
||||||
const logger = require('../../logger')
|
const logger = require('../../logger')
|
||||||
|
|
||||||
const schemaCache = {}
|
let schemaCache = Date.now()
|
||||||
|
|
||||||
const cleanUserSessions = (cleanInterval) => (req, res, next) => {
|
const cleanUserSessions = (cleanInterval) => (req, res, next) => {
|
||||||
const schema = asyncLocalStorage.getStore() ? asyncLocalStorage.getStore().get('schema') : null
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
if (!schema) return next()
|
if (schemaCache + cleanInterval > now) return next()
|
||||||
if (schema && schemaCache.schema + cleanInterval > now) return next()
|
|
||||||
|
|
||||||
logger.debug(`Clearing expired sessions for schema ${schema}`)
|
logger.debug(`Clearing expired sessions for schema 'public'`)
|
||||||
return db.none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [USER_SESSIONS_TABLE_NAME, now])
|
return db.none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [USER_SESSIONS_TABLE_NAME, now])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
schemaCache.schema = now
|
schemaCache = now
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
.catch(next)
|
.catch(next)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const { AuthenticationError } = require('apollo-server-express')
|
|
||||||
const base64 = require('base-64')
|
|
||||||
const users = require('../../users')
|
const users = require('../../users')
|
||||||
|
|
||||||
|
const { AuthenticationError } = require('../graphql/errors')
|
||||||
|
|
||||||
const buildApolloContext = async ({ req, res }) => {
|
const buildApolloContext = async ({ req, res }) => {
|
||||||
if (!req.session.user) return { req, res }
|
if (!req.session.user) return { req, res }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ router.use('*', async (req, res, next) => getOperatorId('authentication').then(o
|
||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
sameSite: true,
|
sameSite: true
|
||||||
maxAge: 60 * 10 * 1000 // 10 minutes
|
|
||||||
}
|
}
|
||||||
})(req, res, next))
|
})(req, res, next))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const machineLoader = require('../../machine-loader')
|
const machineLoader = require('../../machine-loader')
|
||||||
const { UserInputError } = require('apollo-server-express')
|
const { UserInputError } = require('../graphql/errors')
|
||||||
|
|
||||||
function getMachine (machineId) {
|
function getMachine (machineId) {
|
||||||
return machineLoader.getMachines()
|
return machineLoader.getMachines()
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,6 @@ const { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/
|
||||||
|
|
||||||
const NUM_RESULTS = 1000
|
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) {
|
function addProfits (txs) {
|
||||||
return _.map(it => {
|
return _.map(it => {
|
||||||
const profit = getProfit(it).toString()
|
const profit = getProfit(it).toString()
|
||||||
|
|
@ -33,14 +20,31 @@ function addProfits (txs) {
|
||||||
|
|
||||||
const camelize = _.mapKeys(_.camelCase)
|
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 (
|
function batch (
|
||||||
from = new Date(0).toISOString(),
|
from = new Date(0).toISOString(),
|
||||||
until = new Date().toISOString(),
|
until = new Date().toISOString(),
|
||||||
limit = null,
|
limit = null,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
id = null,
|
|
||||||
txClass = null,
|
txClass = null,
|
||||||
machineName = null,
|
deviceId = null,
|
||||||
customerName = null,
|
customerName = null,
|
||||||
fiatCode = null,
|
fiatCode = null,
|
||||||
cryptoCode = null,
|
cryptoCode = null,
|
||||||
|
|
@ -61,8 +65,7 @@ function batch (
|
||||||
k
|
k
|
||||||
)
|
)
|
||||||
)),
|
)),
|
||||||
addProfits,
|
addProfits
|
||||||
addNames
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
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_at AS tx_customer_photo_at,
|
||||||
txs.tx_customer_photo_path AS tx_customer_photo_path,
|
txs.tx_customer_photo_path AS tx_customer_photo_path,
|
||||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
|
((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
|
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 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
|
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
WHERE txs.created >= $2 AND txs.created <= $3
|
||||||
id !== null ? `AND txs.device_id = $6` : ``
|
AND ($6 is null or $6 = 'Cash In')
|
||||||
}
|
AND ($7 is null or txs.device_id = $7)
|
||||||
AND ($7 is null or $7 = 'Cash In')
|
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
|
||||||
AND ($8 is null or d.name = $8)
|
AND ($9 is null or txs.fiat_code = $9)
|
||||||
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.crypto_code = $10)
|
||||||
AND ($10 is null or txs.fiat_code = $10)
|
AND ($11 is null or txs.to_address = $11)
|
||||||
AND ($11 is null or txs.crypto_code = $11)
|
AND ($12 is null or txs.txStatus = $12)
|
||||||
AND ($12 is null or txs.to_address = $12)
|
|
||||||
AND ($13 is null or txs.txStatus = $13)
|
|
||||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
${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)'}
|
${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`
|
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,
|
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_at AS tx_customer_photo_at,
|
||||||
txs.tx_customer_photo_path AS tx_customer_photo_path,
|
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
|
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
|
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||||
AND actions.action = 'provisionAddress'
|
AND actions.action = 'provisionAddress'
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
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}
|
||||||
WHERE txs.created >= $2 AND txs.created <= $3 ${
|
WHERE txs.created >= $2 AND txs.created <= $3
|
||||||
id !== null ? `AND txs.device_id = $6` : ``
|
AND ($6 is null or $6 = 'Cash Out')
|
||||||
}
|
AND ($7 is null or txs.device_id = $7)
|
||||||
AND ($7 is null or $7 = 'Cash Out')
|
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
|
||||||
AND ($8 is null or d.name = $8)
|
AND ($9 is null or txs.fiat_code = $9)
|
||||||
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.crypto_code = $10)
|
||||||
AND ($10 is null or txs.fiat_code = $10)
|
AND ($11 is null or txs.to_address = $11)
|
||||||
AND ($11 is null or txs.crypto_code = $11)
|
AND ($12 is null or txs.txStatus = $12)
|
||||||
AND ($12 is null or txs.to_address = $12)
|
AND ($13 is null or txs.swept = $13)
|
||||||
AND ($13 is null or txs.txStatus = $13)
|
|
||||||
AND ($14 is null or txs.swept = $14)
|
|
||||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||||
${isCsvExport ? '' : 'AND fiat > 0'}
|
${isCsvExport ? '' : 'AND fiat > 0'}
|
||||||
ORDER BY created DESC limit $4 offset $5`
|
ORDER BY created DESC limit $4 offset $5`
|
||||||
|
|
@ -141,13 +142,13 @@ function batch (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCashInOnlyFilters) {
|
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) {
|
} 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 {
|
} else {
|
||||||
promises = [
|
promises = [
|
||||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
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, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])
|
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) {
|
function getCustomerTransactionsBatch (ids) {
|
||||||
const packager = _.flow(it => {
|
const packager = _.flow(it => {
|
||||||
return 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.*,
|
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||||
c.phone AS customer_phone,
|
c.phone AS customer_phone,
|
||||||
|
|
@ -261,9 +262,11 @@ function getCustomerTransactionsBatch (ids) {
|
||||||
c.front_camera_path AS customer_front_camera_path,
|
c.front_camera_path AS customer_front_camera_path,
|
||||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $2)) AS expired,
|
((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
|
FROM cash_in_txs AS txs
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
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
|
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||||
WHERE c.id IN ($1^)
|
WHERE c.id IN ($1^)
|
||||||
ORDER BY created DESC limit $3`
|
ORDER BY created DESC limit $3`
|
||||||
|
|
@ -279,11 +282,13 @@ function getCustomerTransactionsBatch (ids) {
|
||||||
c.name AS customer_name,
|
c.name AS customer_name,
|
||||||
c.front_camera_path AS customer_front_camera_path,
|
c.front_camera_path AS customer_front_camera_path,
|
||||||
c.id_card_photo_path AS customer_id_card_photo_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
|
FROM cash_out_txs txs
|
||||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||||
AND actions.action = 'provisionAddress'
|
AND actions.action = 'provisionAddress'
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||||
|
${DEVICE_NAME_JOINS}
|
||||||
WHERE c.id IN ($1^)
|
WHERE c.id IN ($1^)
|
||||||
ORDER BY created DESC limit $2`
|
ORDER BY created DESC limit $2`
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|
@ -297,7 +302,7 @@ function getCustomerTransactionsBatch (ids) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function single (txId) {
|
function single (txId) {
|
||||||
const packager = _.flow(_.compact, _.map(camelize), addNames)
|
const packager = _.flow(_.compact, _.map(camelize))
|
||||||
|
|
||||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||||
c.phone AS customer_phone,
|
c.phone AS customer_phone,
|
||||||
|
|
@ -309,9 +314,11 @@ function single (txId) {
|
||||||
c.front_camera_path AS customer_front_camera_path,
|
c.front_camera_path AS customer_front_camera_path,
|
||||||
c.id_card_photo_path AS customer_id_card_photo_path,
|
c.id_card_photo_path AS customer_id_card_photo_path,
|
||||||
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
|
((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
|
FROM cash_in_txs AS txs
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
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
|
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
|
||||||
WHERE id=$2`
|
WHERE id=$2`
|
||||||
|
|
||||||
|
|
@ -325,13 +332,14 @@ function single (txId) {
|
||||||
c.id_card_data AS customer_id_card_data,
|
c.id_card_data AS customer_id_card_data,
|
||||||
c.name AS customer_name,
|
c.name AS customer_name,
|
||||||
c.front_camera_path AS customer_front_camera_path,
|
c.front_camera_path AS customer_front_camera_path,
|
||||||
|
|
||||||
c.id_card_photo_path AS customer_id_card_photo_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
|
FROM cash_out_txs txs
|
||||||
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
|
||||||
AND actions.action = 'provisionAddress'
|
AND actions.action = 'provisionAddress'
|
||||||
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
|
||||||
|
${DEVICE_NAME_JOINS}
|
||||||
WHERE id=$1`
|
WHERE id=$1`
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,9 @@ const crypto = require('crypto')
|
||||||
|
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const migration = require('./config-migration')
|
|
||||||
const { asyncLocalStorage } = require('./async-storage')
|
|
||||||
const { getOperatorId } = require('./operator')
|
const { getOperatorId } = require('./operator')
|
||||||
const { getTermsConditions, setTermsConditions } = require('./new-config-manager')
|
const { getTermsConditions, setTermsConditions } = require('./new-config-manager')
|
||||||
|
|
||||||
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
|
|
||||||
const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2
|
const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2
|
||||||
const PASSWORD_FILLED = 'PASSWORD_FILLED'
|
const PASSWORD_FILLED = 'PASSWORD_FILLED'
|
||||||
const SECRET_FIELDS = [
|
const SECRET_FIELDS = [
|
||||||
|
|
@ -59,10 +56,14 @@ const addTermsHash = configs => {
|
||||||
const notifyReload = (dbOrTx, operatorId) =>
|
const notifyReload = (dbOrTx, operatorId) =>
|
||||||
dbOrTx.none(
|
dbOrTx.none(
|
||||||
'NOTIFY $1:name, $2',
|
'NOTIFY $1:name, $2',
|
||||||
['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]
|
['reload', JSON.stringify({ operatorId })]
|
||||||
)
|
)
|
||||||
|
|
||||||
function saveAccounts (accounts) {
|
function saveAccounts (accounts) {
|
||||||
|
if (!accounts) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
const accountsSql = `UPDATE user_config SET data = $1, valid = TRUE, schema_version = $2 WHERE type = 'accounts';
|
const accountsSql = `UPDATE user_config SET data = $1, valid = TRUE, schema_version = $2 WHERE type = 'accounts';
|
||||||
INSERT INTO user_config (type, data, valid, schema_version)
|
INSERT INTO user_config (type, data, valid, schema_version)
|
||||||
SELECT 'accounts', $1, TRUE, $2 WHERE 'accounts' NOT IN (SELECT type FROM user_config)`
|
SELECT 'accounts', $1, TRUE, $2 WHERE 'accounts' NOT IN (SELECT type FROM user_config)`
|
||||||
|
|
|
||||||
|
|
@ -456,13 +456,6 @@ function plugins (settings, deviceId) {
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pong () {
|
|
||||||
return db.none(`UPDATE server_events SET created=now() WHERE event_type=$1;
|
|
||||||
INSERT INTO server_events (event_type) SELECT $1
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, ['ping'])
|
|
||||||
.catch(logger.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Trader functions
|
* Trader functions
|
||||||
*/
|
*/
|
||||||
|
|
@ -935,7 +928,6 @@ function plugins (settings, deviceId) {
|
||||||
getPhoneCode,
|
getPhoneCode,
|
||||||
getEmailCode,
|
getEmailCode,
|
||||||
executeTrades,
|
executeTrades,
|
||||||
pong,
|
|
||||||
clearOldLogs,
|
clearOldLogs,
|
||||||
notifyConfirmation,
|
notifyConfirmation,
|
||||||
sweepHd,
|
sweepHd,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const binance = require('../exchange/binance')
|
||||||
const bitfinex = require('../exchange/bitfinex')
|
const bitfinex = require('../exchange/bitfinex')
|
||||||
const logger = require('../../logger')
|
const logger = require('../../logger')
|
||||||
|
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, TRX, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, TRX, USDT_TRON, LN, USDC } = COINS
|
||||||
|
|
||||||
const ALL = {
|
const ALL = {
|
||||||
cex: cex,
|
cex: cex,
|
||||||
|
|
@ -21,11 +21,6 @@ const ALL = {
|
||||||
bitstamp: bitstamp,
|
bitstamp: bitstamp,
|
||||||
itbit: itbit,
|
itbit: itbit,
|
||||||
bitpay: bitpay,
|
bitpay: bitpay,
|
||||||
coinbase: {
|
|
||||||
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX, LN],
|
|
||||||
FIAT: 'ALL_CURRENCIES',
|
|
||||||
DEFAULT_FIAT_MARKET: 'EUR'
|
|
||||||
},
|
|
||||||
binance: binance,
|
binance: binance,
|
||||||
bitfinex: bitfinex
|
bitfinex: bitfinex
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN, USDC } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN, USDC]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
const DEFAULT_FIAT_MARKET = 'USD'
|
const DEFAULT_FIAT_MARKET = 'USD'
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
const { BTC, ETH, LTC, BCH, USDT, LN, USDC } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN, USDC]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 8
|
const AMOUNT_PRECISION = 8
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
const { BTC, ETH, LTC, BCH, USDT, LN, USDC } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN, USDC]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 8
|
const AMOUNT_PRECISION = 8
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
|
||||||
const { COINS } = require('@lamassu/coins')
|
const { COINS } = require('@lamassu/coins')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN, USDC } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN, USDC]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 6
|
const AMOUNT_PRECISION = 6
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
||||||
.then(([gas, txCount, baseFeePerGas]) => {
|
.then(([gas, txCount, baseFeePerGas]) => {
|
||||||
lastUsedNonces[fromAddress] = txCount
|
lastUsedNonces[fromAddress] = txCount
|
||||||
|
|
||||||
const maxPriorityFeePerGas = new BN(web3.utils.toWei('2.5', 'gwei')) // web3 default value
|
const maxPriorityFeePerGas = new BN(web3.utils.toWei('1.0', 'gwei')) // web3 default value
|
||||||
const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas)
|
const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas)
|
||||||
|
|
||||||
if (includesFee && (toSend.isNegative() || toSend.isZero())) {
|
if (includesFee && (toSend.isNegative() || toSend.isZero())) {
|
||||||
|
|
@ -219,13 +219,11 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId)
|
||||||
.then(([gas, gasPrice, txCount, baseFeePerGas]) => {
|
.then(([gas, gasPrice, txCount, baseFeePerGas]) => {
|
||||||
lastUsedNonces[fromAddress] = txCount
|
lastUsedNonces[fromAddress] = txCount
|
||||||
|
|
||||||
const maxPriorityFeePerGas = new BN(web3.utils.toWei('2.5', 'gwei')) // web3 default value
|
const maxPriorityFeePerGas = new BN(web3.utils.toWei('1.0', 'gwei')) // web3 default value
|
||||||
const neededPriority = new BN(web3.utils.toWei('2.0', 'gwei'))
|
const maxFeePerGas = baseFeePerGas.times(2).plus(maxPriorityFeePerGas)
|
||||||
const maxFeePerGas = baseFeePerGas.plus(neededPriority)
|
|
||||||
const newGasPrice = BN.minimum(maxFeePerGas, baseFeePerGas.plus(maxPriorityFeePerGas))
|
|
||||||
|
|
||||||
const toSend = includesFee
|
const toSend = includesFee
|
||||||
? new BN(amount).minus(newGasPrice.times(gas))
|
? new BN(amount).minus(maxFeePerGas.times(gas))
|
||||||
: amount
|
: amount
|
||||||
|
|
||||||
const rawTx = {
|
const rawTx = {
|
||||||
|
|
|
||||||
132
lib/poller.js
132
lib/poller.js
|
|
@ -8,16 +8,13 @@ const T = require('./time')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const cashOutTx = require('./cash-out/cash-out-tx')
|
const cashOutTx = require('./cash-out/cash-out-tx')
|
||||||
const cashInTx = require('./cash-in/cash-in-tx')
|
const cashInTx = require('./cash-in/cash-in-tx')
|
||||||
const customers = require('./customers')
|
|
||||||
const sanctionsUpdater = require('./ofac/update')
|
const sanctionsUpdater = require('./ofac/update')
|
||||||
const sanctions = require('./ofac/index')
|
const sanctions = require('./ofac/index')
|
||||||
const coinAtmRadar = require('./coinatmradar/coinatmradar')
|
const coinAtmRadar = require('./coinatmradar/coinatmradar')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const complianceTriggers = require('./compliance-triggers')
|
const complianceTriggers = require('./compliance-triggers')
|
||||||
const { asyncLocalStorage, defaultStore } = require('./async-storage')
|
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
const NodeCache = require('node-cache')
|
const NodeCache = require('node-cache')
|
||||||
const util = require('util')
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const processBatches = require('./tx-batching-processing')
|
const processBatches = require('./tx-batching-processing')
|
||||||
|
|
||||||
|
|
@ -26,7 +23,6 @@ const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
|
||||||
const UNNOTIFIED_INTERVAL = 10 * T.seconds
|
const UNNOTIFIED_INTERVAL = 10 * T.seconds
|
||||||
const SWEEP_HD_INTERVAL = 5 * T.minute
|
const SWEEP_HD_INTERVAL = 5 * T.minute
|
||||||
const TRADE_INTERVAL = 60 * T.seconds
|
const TRADE_INTERVAL = 60 * T.seconds
|
||||||
const PONG_INTERVAL = 10 * T.seconds
|
|
||||||
const LOGS_CLEAR_INTERVAL = 1 * T.day
|
const LOGS_CLEAR_INTERVAL = 1 * T.day
|
||||||
const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes
|
const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes
|
||||||
const SANCTIONS_UPDATE_INTERVAL = 1 * T.day
|
const SANCTIONS_UPDATE_INTERVAL = 1 * T.day
|
||||||
|
|
@ -56,17 +52,11 @@ const SLOW_QUEUE = new Queue({
|
||||||
interval: SLOW_QUEUE_WAIT
|
interval: SLOW_QUEUE_WAIT
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fix for asyncLocalStorage store being lost due to callback-based queue
|
|
||||||
FAST_QUEUE.enqueue = util.promisify(FAST_QUEUE.enqueue)
|
|
||||||
SLOW_QUEUE.enqueue = util.promisify(SLOW_QUEUE.enqueue)
|
|
||||||
|
|
||||||
const QUEUE = {
|
const QUEUE = {
|
||||||
FAST: FAST_QUEUE,
|
FAST: FAST_QUEUE,
|
||||||
SLOW: SLOW_QUEUE
|
SLOW: SLOW_QUEUE
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaCallbacks = new Map()
|
|
||||||
|
|
||||||
const cachedVariables = new NodeCache({
|
const cachedVariables = new NodeCache({
|
||||||
stdTTL: CACHE_ENTRY_TTL,
|
stdTTL: CACHE_ENTRY_TTL,
|
||||||
checkperiod: CACHE_ENTRY_TTL,
|
checkperiod: CACHE_ENTRY_TTL,
|
||||||
|
|
@ -78,31 +68,25 @@ cachedVariables.on('expired', (key, val) => {
|
||||||
if (!val.isReloading) {
|
if (!val.isReloading) {
|
||||||
// since val is passed by reference we don't need to do cachedVariables.set()
|
// since val is passed by reference we don't need to do cachedVariables.set()
|
||||||
val.isReloading = true
|
val.isReloading = true
|
||||||
return reload(key)
|
return reload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
db.connect({ direct: true }).then(sco => {
|
db.connect({ direct: true }).then(sco => {
|
||||||
sco.client.on('notification', data => {
|
sco.client.on('notification', () => {
|
||||||
const parsedData = JSON.parse(data.payload)
|
return reload()
|
||||||
return reload(parsedData.schema)
|
|
||||||
})
|
})
|
||||||
return sco.none('LISTEN $1:name', 'reload')
|
return sco.none('LISTEN $1:name', 'reload')
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
|
|
||||||
function reload (schema) {
|
function reload () {
|
||||||
const store = defaultStore()
|
return settingsLoader.loadLatest()
|
||||||
store.set('schema', schema)
|
.then(settings => {
|
||||||
// set asyncLocalStorage so settingsLoader loads settings for the right schema
|
const pi = plugins(settings)
|
||||||
return asyncLocalStorage.run(store, () => {
|
cachedVariables.set('public', { settings, pi, isReloading: false })
|
||||||
return settingsLoader.loadLatest()
|
logger.debug(`Settings for schema 'public' reloaded in poller`)
|
||||||
.then(settings => {
|
return updateAndLoadSanctions()
|
||||||
const pi = plugins(settings)
|
})
|
||||||
cachedVariables.set(schema, { settings, pi, isReloading: false })
|
|
||||||
logger.debug(`Settings for schema '${schema}' reloaded in poller`)
|
|
||||||
return updateAndLoadSanctions()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pi () { return cachedVariables.get('public').pi }
|
function pi () { return cachedVariables.get('public').pi }
|
||||||
|
|
@ -205,26 +189,12 @@ const cleanOldFailedQRScans = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// function checkExternalCompliance (settings) {
|
function setup () {
|
||||||
// return customers.checkExternalCompliance(settings)
|
return settingsLoader.loadLatest().then(settings => {
|
||||||
// }
|
const pi = plugins(settings)
|
||||||
|
cachedVariables.set('public', { settings, pi, isReloading: false })
|
||||||
function initializeEachSchema (schemas = ['public']) {
|
return doPolling()
|
||||||
// for each schema set "thread variables" and do polling
|
}).catch(console.error)
|
||||||
return _.forEach(schema => {
|
|
||||||
const store = defaultStore()
|
|
||||||
store.set('schema', schema)
|
|
||||||
return asyncLocalStorage.run(store, () => {
|
|
||||||
return settingsLoader.loadLatest().then(settings => {
|
|
||||||
// prevent inadvertedly clearing the array without clearing timeouts
|
|
||||||
if (schemaCallbacks.has(schema)) throw new Error(`The schema "${schema}" cannot be initialized twice on poller`)
|
|
||||||
const pi = plugins(settings)
|
|
||||||
cachedVariables.set(schema, { settings, pi, isReloading: false })
|
|
||||||
schemaCallbacks.set(schema, [])
|
|
||||||
return doPolling(schema)
|
|
||||||
})
|
|
||||||
}).catch(console.error)
|
|
||||||
}, schemas)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recursiveTimeout (func, timeout, ...vars) {
|
function recursiveTimeout (func, timeout, ...vars) {
|
||||||
|
|
@ -246,25 +216,12 @@ function recursiveTimeout (func, timeout, ...vars) {
|
||||||
}, timeout)
|
}, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToQueue (func, interval, schema, queue, ...vars) {
|
function addToQueue (func, interval, queue, ...vars) {
|
||||||
recursiveTimeout(func, interval, ...vars)
|
recursiveTimeout(func, interval, ...vars)
|
||||||
// return schemaCallbacks.get(schema).push(setInterval(() => {
|
|
||||||
// return queue.enqueue().then(() => {
|
|
||||||
// // get plugins or settings from the cache every time func is run
|
|
||||||
// const loadVariables = vars.length > 0 && typeof vars[0] === 'function'
|
|
||||||
// if (loadVariables) {
|
|
||||||
// const funcVars = [...vars]
|
|
||||||
// funcVars[0] = vars[0]()
|
|
||||||
// return func(...funcVars)
|
|
||||||
// }
|
|
||||||
// return func(...vars)
|
|
||||||
// }).catch(console.error)
|
|
||||||
// }, interval))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function doPolling (schema) {
|
function doPolling () {
|
||||||
pi().executeTrades()
|
pi().executeTrades()
|
||||||
pi().pong()
|
|
||||||
pi().clearOldLogs()
|
pi().clearOldLogs()
|
||||||
cashOutTx.monitorLiveIncoming(settings())
|
cashOutTx.monitorLiveIncoming(settings())
|
||||||
cashOutTx.monitorStaleIncoming(settings())
|
cashOutTx.monitorStaleIncoming(settings())
|
||||||
|
|
@ -272,40 +229,23 @@ function doPolling (schema) {
|
||||||
pi().sweepHd()
|
pi().sweepHd()
|
||||||
notifier.checkNotification(pi())
|
notifier.checkNotification(pi())
|
||||||
updateCoinAtmRadar()
|
updateCoinAtmRadar()
|
||||||
// checkExternalCompliance(settings())
|
|
||||||
|
|
||||||
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, QUEUE.FAST)
|
||||||
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().executeTrades, TRADE_INTERVAL, QUEUE.FAST)
|
||||||
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, QUEUE.FAST, settings)
|
||||||
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, QUEUE.FAST, settings)
|
||||||
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, QUEUE.FAST, settings)
|
||||||
addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, QUEUE.FAST, settings)
|
||||||
addToQueue(processBatches, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE)
|
addToQueue(processBatches, UNNOTIFIED_INTERVAL, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE)
|
||||||
addToQueue(pi().sweepHd, SWEEP_HD_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(pi().sweepHd, SWEEP_HD_INTERVAL, QUEUE.FAST, settings)
|
||||||
addToQueue(pi().pong, PONG_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().clearOldLogs, LOGS_CLEAR_INTERVAL, QUEUE.SLOW)
|
||||||
addToQueue(pi().clearOldLogs, LOGS_CLEAR_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(notifier.checkNotification, CHECK_NOTIFICATION_INTERVAL, QUEUE.FAST, pi)
|
||||||
addToQueue(notifier.checkNotification, CHECK_NOTIFICATION_INTERVAL, schema, QUEUE.FAST, pi)
|
addToQueue(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL, QUEUE.SLOW)
|
||||||
addToQueue(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, QUEUE.SLOW)
|
||||||
addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, QUEUE.SLOW)
|
||||||
addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, QUEUE.SLOW, settings)
|
||||||
addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings)
|
addToQueue(cleanOldFailedQRScans, FAILED_SCANS_INTERVAL, QUEUE.SLOW, settings)
|
||||||
addToQueue(cleanOldFailedQRScans, FAILED_SCANS_INTERVAL, schema, QUEUE.SLOW, settings)
|
addToQueue(cleanOldFailedPDF417Scans, FAILED_SCANS_INTERVAL, QUEUE.SLOW, settings)
|
||||||
addToQueue(cleanOldFailedPDF417Scans, FAILED_SCANS_INTERVAL, schema, QUEUE.SLOW, settings)
|
|
||||||
// addToQueue(checkExternalCompliance, EXTERNAL_COMPLIANCE_INTERVAL, schema, QUEUE.SLOW, settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup (schemasToAdd = [], schemasToRemove = []) {
|
module.exports = { setup, reload }
|
||||||
// clear callback array for each schema in schemasToRemove and clear cached variables
|
|
||||||
_.forEach(schema => {
|
|
||||||
const callbacks = schemaCallbacks.get(schema)
|
|
||||||
_.forEach(clearInterval, callbacks)
|
|
||||||
schemaCallbacks.delete(schema)
|
|
||||||
cachedVariables.del(schema)
|
|
||||||
}, schemasToRemove)
|
|
||||||
|
|
||||||
return initializeEachSchema(schemasToAdd)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActiveSchemas = () => Array.from(schemaCallbacks.keys())
|
|
||||||
|
|
||||||
module.exports = { setup, reload, getActiveSchemas }
|
|
||||||
|
|
|
||||||
138
lib/routes.js
138
lib/routes.js
|
|
@ -1,5 +1,4 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const argv = require('minimist')(process.argv.slice(2))
|
|
||||||
const compression = require('compression')
|
const compression = require('compression')
|
||||||
const helmet = require('helmet')
|
const helmet = require('helmet')
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan')
|
||||||
|
|
@ -9,7 +8,6 @@ const logger = require('./logger')
|
||||||
|
|
||||||
const addRWBytes = require('./middlewares/addRWBytes')
|
const addRWBytes = require('./middlewares/addRWBytes')
|
||||||
const authorize = require('./middlewares/authorize')
|
const authorize = require('./middlewares/authorize')
|
||||||
const computeSchema = require('./middlewares/compute-schema')
|
|
||||||
const errorHandler = require('./middlewares/errorHandler')
|
const errorHandler = require('./middlewares/errorHandler')
|
||||||
const filterOldRequests = require('./middlewares/filterOldRequests')
|
const filterOldRequests = require('./middlewares/filterOldRequests')
|
||||||
const findOperatorId = require('./middlewares/operatorId')
|
const findOperatorId = require('./middlewares/operatorId')
|
||||||
|
|
@ -35,84 +33,96 @@ const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes')
|
||||||
const probeRoutes = require('./routes/probeLnRoutes')
|
const probeRoutes = require('./routes/probeLnRoutes')
|
||||||
const failedQRScansRoutes = require('./routes/failedQRScans')
|
const failedQRScansRoutes = require('./routes/failedQRScans')
|
||||||
|
|
||||||
const graphQLServer = require('./graphql/server')
|
const { graphQLServer, context } = require('./graphql/server')
|
||||||
|
|
||||||
const app = express()
|
const { expressMiddleware } = require('@apollo/server/express4')
|
||||||
|
|
||||||
const configRequiredRoutes = [
|
const loadRoutes = async () => {
|
||||||
'/poll',
|
const app = express()
|
||||||
'/terms_conditions',
|
|
||||||
'/event',
|
|
||||||
'/phone_code',
|
|
||||||
'/customer',
|
|
||||||
'/tx',
|
|
||||||
'/verify_promo_code',
|
|
||||||
'/graphql'
|
|
||||||
]
|
|
||||||
|
|
||||||
// middleware setup
|
const configRequiredRoutes = [
|
||||||
app.use(addRWBytes())
|
'/poll',
|
||||||
app.use(compression({ threshold: 500 }))
|
'/terms_conditions',
|
||||||
app.use(helmet())
|
'/event',
|
||||||
app.use(nocache())
|
'/phone_code',
|
||||||
app.use(express.json({ limit: '2mb' }))
|
'/customer',
|
||||||
|
'/tx',
|
||||||
|
'/verify_promo_code',
|
||||||
|
'/graphql'
|
||||||
|
]
|
||||||
|
|
||||||
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
// middleware setup
|
||||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
app.use(addRWBytes())
|
||||||
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
app.use(compression({ threshold: 500 }))
|
||||||
|
app.use(helmet())
|
||||||
|
app.use(nocache())
|
||||||
|
app.use(express.json({ limit: '2mb' }))
|
||||||
|
|
||||||
app.use('/robots.txt', (req, res) => {
|
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||||
res.type('text/plain')
|
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||||
res.send("User-agent: *\nDisallow: /")
|
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.use('/robots.txt', (req, res) => {
|
||||||
res.sendStatus(404)
|
res.type('text/plain')
|
||||||
})
|
res.send("User-agent: *\nDisallow: /")
|
||||||
|
})
|
||||||
|
|
||||||
// app /pair and /ca routes
|
app.get('/', (req, res) => {
|
||||||
app.use('/', pairingRoutes)
|
res.sendStatus(404)
|
||||||
|
})
|
||||||
|
|
||||||
app.use(findOperatorId)
|
// app /pair and /ca routes
|
||||||
app.use(populateDeviceId)
|
app.use('/', pairingRoutes)
|
||||||
app.use(computeSchema)
|
|
||||||
app.use(authorize)
|
|
||||||
app.use(configRequiredRoutes, populateSettings)
|
|
||||||
app.use(filterOldRequests)
|
|
||||||
|
|
||||||
// other app routes
|
app.use(findOperatorId)
|
||||||
app.use('/graphql', recordPing)
|
app.use(populateDeviceId)
|
||||||
app.use('/poll', pollingRoutes)
|
app.use(authorize)
|
||||||
app.use('/terms_conditions', termsAndConditionsRoutes)
|
app.use(configRequiredRoutes, populateSettings)
|
||||||
app.use('/state', stateRoutes)
|
app.use(filterOldRequests)
|
||||||
app.use('/cashbox', cashboxRoutes)
|
|
||||||
|
|
||||||
app.use('/network', performanceRoutes)
|
// other app routes
|
||||||
app.use('/diagnostics', diagnosticsRoutes)
|
app.use('/graphql', recordPing)
|
||||||
app.use('/failedqrscans', failedQRScansRoutes)
|
app.use('/poll', pollingRoutes)
|
||||||
|
app.use('/terms_conditions', termsAndConditionsRoutes)
|
||||||
|
app.use('/state', stateRoutes)
|
||||||
|
app.use('/cashbox', cashboxRoutes)
|
||||||
|
|
||||||
app.use('/verify_user', verifyUserRoutes)
|
app.use('/network', performanceRoutes)
|
||||||
app.use('/verify_transaction', verifyTxRoutes)
|
app.use('/diagnostics', diagnosticsRoutes)
|
||||||
app.use('/verify_promo_code', verifyPromoCodeRoutes)
|
app.use('/failedqrscans', failedQRScansRoutes)
|
||||||
|
|
||||||
// BACKWARDS_COMPATIBILITY 9.0
|
app.use('/verify_user', verifyUserRoutes)
|
||||||
// machines before 9.0 still use the phone_code route
|
app.use('/verify_transaction', verifyTxRoutes)
|
||||||
app.use('/phone_code', phoneCodeRoutes)
|
app.use('/verify_promo_code', verifyPromoCodeRoutes)
|
||||||
|
|
||||||
app.use('/customer', customerRoutes)
|
// BACKWARDS_COMPATIBILITY 9.0
|
||||||
|
// machines before 9.0 still use the phone_code route
|
||||||
|
app.use('/phone_code', phoneCodeRoutes)
|
||||||
|
|
||||||
app.use('/tx', txRoutes)
|
app.use('/customer', customerRoutes)
|
||||||
|
|
||||||
app.use('/logs', logsRoutes)
|
app.use('/tx', txRoutes)
|
||||||
app.use('/units', unitsRoutes)
|
|
||||||
|
|
||||||
app.use('/probe', probeRoutes)
|
app.use('/logs', logsRoutes)
|
||||||
|
app.use('/units', unitsRoutes)
|
||||||
|
|
||||||
graphQLServer.applyMiddleware({ app })
|
app.use('/probe', probeRoutes)
|
||||||
|
|
||||||
app.use(errorHandler)
|
await graphQLServer.start()
|
||||||
app.use((req, res) => {
|
app.use('/graphql',
|
||||||
res.status(404).json({ error: 'No such route' })
|
express.json(),
|
||||||
})
|
expressMiddleware(graphQLServer, {
|
||||||
|
context,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = { app }
|
app.use(errorHandler)
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'No such route' })
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { loadRoutes }
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@ function cashboxRemoval (req, res, next) {
|
||||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||||
.then(batch => Promise.all([
|
.then(batch => Promise.all([
|
||||||
cashbox.getBatchById(batch.id),
|
cashbox.getBatchById(batch.id),
|
||||||
getMachineName(batch.device_id),
|
getMachineName(batch.device_id)
|
||||||
setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
|
|
||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
|
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const loadOrUpdateSanctions = () => {
|
||||||
sanctionStatus.timestamp = Date.now()
|
sanctionStatus.timestamp = Date.now()
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
logger.error('Couldn\'t load OFAC sanction list!')
|
logger.error('Couldn\'t load OFAC sanction list!', e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
const db = require('./db')
|
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) {
|
exports.up = function (next) {
|
||||||
return migrateTools.migrateNames()
|
const sql = [
|
||||||
.then(updateSql => {
|
'alter table devices add column name text',
|
||||||
const sql = [
|
'alter table devices alter column name set not null'
|
||||||
'alter table devices add column name text',
|
]
|
||||||
updateSql,
|
|
||||||
'alter table devices alter column name set not null'
|
|
||||||
]
|
|
||||||
|
|
||||||
return db.multi(sql, next)
|
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) {
|
exports.down = function (next) {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,9 @@
|
||||||
const db = require('./db')
|
// This migration was actually a config update
|
||||||
const machineLoader = require('../lib/machine-loader')
|
// it's from before 7.5 and we update one major version at a time
|
||||||
const { migrationSaveConfig, saveAccounts, loadLatest } = require('../lib/new-settings-loader')
|
// v10.2 is good enough to deprecate it
|
||||||
const { migrate } = require('../lib/config-migration')
|
// file still has to exist so that the migration tool doesn't throw an error
|
||||||
|
|
||||||
const _ = require('lodash/fp')
|
|
||||||
|
|
||||||
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
|
|
||||||
|
|
||||||
module.exports.up = function (next) {
|
module.exports.up = function (next) {
|
||||||
function migrateConfig (settings) {
|
next()
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.down = function (next) {
|
module.exports.down = function (next) {
|
||||||
|
|
|
||||||
12
migrations/1743526540370-deprecate-tables.js
Normal file
12
migrations/1743526540370-deprecate-tables.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = next => db.multi([
|
||||||
|
'DROP TABLE aggregated_machine_pings;',
|
||||||
|
'DROP TABLE cash_in_refills;',
|
||||||
|
'DROP TABLE cash_out_refills;',
|
||||||
|
'DROP TABLE customer_compliance_persistence;',
|
||||||
|
'DROP TABLE compliance_overrides_persistence;',
|
||||||
|
'DROP TABLE server_events;',
|
||||||
|
], next)
|
||||||
|
|
||||||
|
exports.down = next => next()
|
||||||
10
migrations/1744294267662-bills-performance.js
Normal file
10
migrations/1744294267662-bills-performance.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = next => db.multi([
|
||||||
|
'ALTER TABLE bills ADD CONSTRAINT cash_in_txs_id FOREIGN KEY (cash_in_txs_id) REFERENCES cash_in_txs(id);',
|
||||||
|
'CREATE INDEX bills_cash_in_txs_id_idx ON bills USING btree (cash_in_txs_id);',
|
||||||
|
`CREATE INDEX bills_null_cashbox_batch_id_idx ON bills (cash_in_txs_id) WHERE cashbox_batch_id IS NULL AND destination_unit = 'cashbox';`,
|
||||||
|
'CREATE INDEX cash_in_txs_device_id_idx ON cash_in_txs USING btree (device_id);'
|
||||||
|
], next)
|
||||||
|
|
||||||
|
exports.down = next => next()
|
||||||
11
migrations/1744294267663-blacklist-normalization.js
Normal file
11
migrations/1744294267663-blacklist-normalization.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = next => db.multi([
|
||||||
|
'ALTER TABLE public.blacklist DROP CONSTRAINT IF EXISTS blacklist_pkey;',
|
||||||
|
'ALTER TABLE public.blacklist ADD PRIMARY KEY (address);',
|
||||||
|
'DROP INDEX IF EXISTS blacklist_temp_address_key;',
|
||||||
|
'CREATE UNIQUE INDEX blacklist_address_idx ON public.blacklist USING btree (address);',
|
||||||
|
|
||||||
|
], next)
|
||||||
|
|
||||||
|
exports.down = next => 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')
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
nodejs 22
|
|
||||||
4103
new-lamassu-admin/package-lock.json
generated
4103
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/react-hooks": "^3.1.3",
|
"@apollo/react-hooks": "^3.1.3",
|
||||||
"@lamassu/coins": "v1.5.3",
|
"@lamassu/coins": "v1.6.1",
|
||||||
"@material-ui/core": "4.12.4",
|
"@material-ui/core": "4.12.4",
|
||||||
"@material-ui/icons": "4.11.2",
|
"@material-ui/icons": "4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
"apollo-link-http": "^1.5.17",
|
"apollo-link-http": "^1.5.17",
|
||||||
"apollo-upload-client": "^13.0.0",
|
"apollo-upload-client": "^13.0.0",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"base-64": "^1.0.0",
|
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"countries-and-timezones": "^2.4.0",
|
"countries-and-timezones": "^2.4.0",
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ const SearchBox = memo(
|
||||||
classes={{ option: classes.autocomplete }}
|
classes={{ option: classes.autocomplete }}
|
||||||
value={filters}
|
value={filters}
|
||||||
options={options}
|
options={options}
|
||||||
getOptionLabel={it => it.value}
|
getOptionLabel={it => it.label || it.value}
|
||||||
renderOption={it => (
|
renderOption={it => (
|
||||||
<div className={classes.item}>
|
<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>
|
<P className={classes.itemType}>{it.type}</P>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const SearchFilter = ({
|
||||||
<Chip
|
<Chip
|
||||||
key={idx}
|
key={idx}
|
||||||
classes={chipClasses}
|
classes={chipClasses}
|
||||||
label={`${onlyFirstToUpper(f.type)}: ${f.value}`}
|
label={`${onlyFirstToUpper(f.type)}: ${f.label || f.value}`}
|
||||||
onDelete={() => onFilterDelete(f)}
|
onDelete={() => onFilterDelete(f)}
|
||||||
deleteIcon={<CloseIcon className={classes.button} />}
|
deleteIcon={<CloseIcon className={classes.button} />}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,16 @@ const BooleanCell = ({ name }) => {
|
||||||
const BooleanPropertiesTable = memo(
|
const BooleanPropertiesTable = memo(
|
||||||
({ title, disabled, data, elements, save, forcedEditing = false }) => {
|
({ title, disabled, data, elements, save, forcedEditing = false }) => {
|
||||||
const initialValues = R.fromPairs(
|
const initialValues = R.fromPairs(
|
||||||
elements.map(it => [it.name, data[it.name]?.toString() ?? null])
|
elements.map(it => [it.name, data[it.name]?.toString() ?? 'false'])
|
||||||
)
|
)
|
||||||
|
|
||||||
const validationSchema = R.fromPairs(
|
const validationSchema = Yup.object().shape(
|
||||||
elements.map(it => [it.name, Yup.boolean().required()])
|
R.fromPairs(
|
||||||
|
elements.map(it => [
|
||||||
|
it.name,
|
||||||
|
Yup.mixed().oneOf(['true', 'false', true, false]).required()
|
||||||
|
])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const [editing, setEditing] = useState(forcedEditing)
|
const [editing, setEditing] = useState(forcedEditing)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const Td = ({
|
||||||
[classes.size]: !header,
|
[classes.size]: !header,
|
||||||
[classes.bold]: !header && bold
|
[classes.bold]: !header && bold
|
||||||
}
|
}
|
||||||
return <div className={classnames(className, classNames)}>{children}</div>
|
return <div data-cy={`td-${header}`} className={classnames(className, classNames)}>{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Th = ({ children, ...props }) => {
|
const Th = ({ children, ...props }) => {
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ const MACHINE_LOGS = gql`
|
||||||
query machineLogsCsv(
|
query machineLogsCsv(
|
||||||
$deviceId: ID!
|
$deviceId: ID!
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$from: Date
|
$from: DateTimeISO
|
||||||
$until: Date
|
$until: DateTimeISO
|
||||||
$timezone: String
|
$timezone: String
|
||||||
) {
|
) {
|
||||||
machineLogsCsv(
|
machineLogsCsv(
|
||||||
|
|
@ -52,7 +52,6 @@ const MACHINE_LOGS = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const createCsv = async ({ machineLogsCsv }) => {
|
const createCsv = async ({ machineLogsCsv }) => {
|
||||||
console.log(machineLogsCsv)
|
|
||||||
const machineLogs = new Blob([machineLogsCsv], {
|
const machineLogs = new Blob([machineLogsCsv], {
|
||||||
type: 'text/plain;charset=utf-8'
|
type: 'text/plain;charset=utf-8'
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ const Row = ({
|
||||||
return (
|
return (
|
||||||
<div className={classes.rowWrapper}>
|
<div className={classes.rowWrapper}>
|
||||||
<div
|
<div
|
||||||
|
data-cy={id}
|
||||||
className={classnames({ [classes.before]: expanded && index !== 0 })}>
|
className={classnames({ [classes.before]: expanded && index !== 0 })}>
|
||||||
<Tr
|
<Tr
|
||||||
size={size}
|
size={size}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ const DAY_OPTIONS = R.map(
|
||||||
|
|
||||||
const GET_TRANSACTIONS = gql`
|
const GET_TRANSACTIONS = gql`
|
||||||
query transactions(
|
query transactions(
|
||||||
$from: Date
|
$from: DateTimeISO
|
||||||
$until: Date
|
$until: DateTimeISO
|
||||||
$excludeTestingCustomers: Boolean
|
$excludeTestingCustomers: Boolean
|
||||||
) {
|
) {
|
||||||
transactions(
|
transactions(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||||
import ReverseSettingsIcon from 'src/styling/icons/circle buttons/settings/white.svg?react'
|
import ReverseSettingsIcon from 'src/styling/icons/circle buttons/settings/white.svg?react'
|
||||||
import SettingsIcon from 'src/styling/icons/circle buttons/settings/zodiac.svg?react'
|
import SettingsIcon from 'src/styling/icons/circle buttons/settings/zodiac.svg?react'
|
||||||
|
|
||||||
import { Link, Button, IconButton } from 'src/components/buttons'
|
import { Link, Button, IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
|
|
||||||
|
|
@ -275,10 +275,13 @@ const Blacklist = () => {
|
||||||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||||
<HelpTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
This option requires a user to scan a fresh wallet address if
|
For details about rejecting address reuse, please read the
|
||||||
they attempt to scan one that had been previously used for a
|
relevant knowledgebase article:
|
||||||
transaction in your network.
|
|
||||||
</P>
|
</P>
|
||||||
|
<SupportLinkButton
|
||||||
|
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
|
||||||
|
label="Reject Address Reuse"
|
||||||
|
/>
|
||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
/>
|
/>
|
||||||
<SupportLinkButton
|
<SupportLinkButton
|
||||||
link="https://support.lamassu.is/hc/en-us/articles/360061558352-Commissions-and-Profit-Calculations"
|
link="https://support.lamassu.is/hc/en-us/articles/360061558352-Commissions-and-Profit-Calculations"
|
||||||
label="SCommissions and Profit Calculations"
|
label="Commissions and Profit Calculations"
|
||||||
bottomSpace="1"
|
bottomSpace="1"
|
||||||
/>
|
/>
|
||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
|
|
|
||||||
|
|
@ -147,19 +147,25 @@ const Wizard = ({
|
||||||
onSubmit={onContinue}
|
onSubmit={onContinue}
|
||||||
initialValues={stepOptions.initialValues}
|
initialValues={stepOptions.initialValues}
|
||||||
validationSchema={stepOptions.schema}>
|
validationSchema={stepOptions.schema}>
|
||||||
<Form className={classes.form}>
|
{({ errors }) => (
|
||||||
<stepOptions.Component
|
<Form className={classes.form}>
|
||||||
selectedValues={selectedValues}
|
<stepOptions.Component
|
||||||
customInfoRequirementOptions={customInfoRequirementOptions}
|
selectedValues={selectedValues}
|
||||||
{...stepOptions.props}
|
customInfoRequirementOptions={customInfoRequirementOptions}
|
||||||
/>
|
errors={errors}
|
||||||
<div className={classes.submit}>
|
{...stepOptions.props}
|
||||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
/>
|
||||||
<Button className={classes.button} type="submit">
|
<div className={classes.submit}>
|
||||||
{isLastStep ? 'Add Data' : 'Next'}
|
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||||
</Button>
|
{Object.keys(errors).length > 0 && (
|
||||||
</div>
|
<ErrorMessage>{Object.values(errors)[0]}</ErrorMessage>
|
||||||
</Form>
|
)}
|
||||||
|
<Button className={classes.button} type="submit">
|
||||||
|
{isLastStep ? 'Add Data' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -453,14 +453,16 @@ const customerDataSchemas = {
|
||||||
documentNumber: Yup.string().required(),
|
documentNumber: Yup.string().required(),
|
||||||
dateOfBirth: Yup.string()
|
dateOfBirth: Yup.string()
|
||||||
.test({
|
.test({
|
||||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val)),
|
||||||
|
message: 'Date must be in format YYYY-MM-DD'
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
gender: Yup.string().required(),
|
gender: Yup.string().required(),
|
||||||
country: Yup.string().required(),
|
country: Yup.string().required(),
|
||||||
expirationDate: Yup.string()
|
expirationDate: Yup.string()
|
||||||
.test({
|
.test({
|
||||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val)),
|
||||||
|
message: 'Date must be in format YYYY-MM-DD'
|
||||||
})
|
})
|
||||||
.required()
|
.required()
|
||||||
}),
|
}),
|
||||||
|
|
@ -543,9 +545,12 @@ const tryFormatDate = rawDate => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDates = values => {
|
const formatDates = values => {
|
||||||
R.forEach(elem => {
|
R.map(
|
||||||
values[elem] = tryFormatDate(values[elem])
|
elem =>
|
||||||
})(['dateOfBirth', 'expirationDate'])
|
(values[elem] = format('yyyyMMdd')(
|
||||||
|
parse(new Date(), 'yyyy-MM-dd', values[elem])
|
||||||
|
))
|
||||||
|
)(['dateOfBirth', 'expirationDate'])
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import Section from 'src/components/layout/Section'
|
import Section from 'src/components/layout/Section'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { P } from 'src/components/typography'
|
import { P } from 'src/components/typography'
|
||||||
|
import _schemas from 'src/pages/Services/schemas'
|
||||||
import Wizard from 'src/pages/Wallet/Wizard'
|
import Wizard from 'src/pages/Wallet/Wizard'
|
||||||
import { WalletSchema } from 'src/pages/Wallet/helper'
|
import { WalletSchema } from 'src/pages/Wallet/helper'
|
||||||
|
|
||||||
|
|
@ -68,6 +69,12 @@ const SAVE_CONFIG = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GET_MARKETS = gql`
|
||||||
|
query getMarkets {
|
||||||
|
getMarkets
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const FiatCurrencyChangeAlert = ({ open, close, save }) => {
|
const FiatCurrencyChangeAlert = ({ open, close, save }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -107,6 +114,9 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
const [isEditingDefault, setEditingDefault] = useState(false)
|
const [isEditingDefault, setEditingDefault] = useState(false)
|
||||||
const [isEditingOverrides, setEditingOverrides] = useState(false)
|
const [isEditingOverrides, setEditingOverrides] = useState(false)
|
||||||
const { data } = useQuery(GET_DATA)
|
const { data } = useQuery(GET_DATA)
|
||||||
|
const { data: marketsData } = useQuery(GET_MARKETS)
|
||||||
|
const schemas = _schemas(marketsData?.getMarkets)
|
||||||
|
|
||||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||||
onCompleted: () => setWizard(false),
|
onCompleted: () => setWizard(false),
|
||||||
refetchQueries: () => ['getData'],
|
refetchQueries: () => ['getData'],
|
||||||
|
|
@ -234,6 +244,7 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
</Section>
|
</Section>
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
|
schemas={schemas}
|
||||||
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
||||||
onClose={() => setWizard(false)}
|
onClose={() => setWizard(false)}
|
||||||
save={wizardSave}
|
save={wizardSave}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue