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:
siiky 2025-04-15 12:43:30 +01:00
commit 5d24f9b889
124 changed files with 17979 additions and 15339 deletions

View file

@ -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:

View file

@ -1 +1 @@
nodejs 14 nodejs 22

View file

@ -4,27 +4,16 @@ 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()
const store = defaultStore()
asyncLocalStorage.run(store, () => {
db.none(createMigration) db.none(createMigration)
.then(() => Promise.all([db.oneOrNone(select), getMigrateFile()]))
.then(([qResult, migrateFile]) => {
process.env.SKIP_SERVER_LOGS = !(qResult && _.find(({ title }) => title === '1572524820075-server-support-logs.js', qResult.data.migrations ?? []))
if (!qResult && migrateFile) {
return db.none('insert into migrations (id, data) values (1, $1)', [migrateFile])
}
})
.then(() => migrate.run()) .then(() => migrate.run())
.then(() => { .then(() => {
console.log('DB Migration succeeded.') console.log('DB Migration succeeded.')
@ -34,4 +23,3 @@ asyncLocalStorage.run(store, () => {
console.error('DB Migration failed: %s', err) console.error('DB Migration failed: %s', err)
process.exit(1) process.exit(1)
}) })
})

View file

@ -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,7 +31,6 @@ 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}`)
@ -51,4 +49,3 @@ asyncLocalStorage.run(defaultStore(), () => {
console.log('Error: %s', err) console.log('Error: %s', err)
process.exit(3) process.exit(3)
}) })
})

View file

@ -1,5 +0,0 @@
#!/usr/bin/env node
const adminServer = require('../lib/new-admin/graphql-dev-insecure')
adminServer.run()

View file

@ -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

View file

@ -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

View file

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

View file

@ -1,191 +0,0 @@
const _ = require('lodash/fp')
const db = require('../db')
const configManager = require('./config-manager')
const logger = require('../logger')
const schema = require('./lamassu-schema.json')
const REMOVED_FIELDS = ['crossRefVerificationActive', 'crossRefVerificationThreshold']
const SETTINGS_LOADER_SCHEMA_VERSION = 1
function allScopes (cryptoScopes, machineScopes) {
const scopes = []
cryptoScopes.forEach(c => {
machineScopes.forEach(m => scopes.push([c, m]))
})
return scopes
}
function allCryptoScopes (cryptos, cryptoScope) {
const cryptoScopes = []
if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global')
if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r))
return cryptoScopes
}
function allMachineScopes (machineList, machineScope) {
const machineScopes = []
if (machineScope === 'global' || machineScope === 'both') machineScopes.push('global')
if (machineScope === 'specific' || machineScope === 'both') machineList.forEach(r => machineScopes.push(r))
return machineScopes
}
function satisfiesRequire (config, cryptos, machineList, field, anyFields, allFields) {
const fieldCode = field.code
const scopes = allScopes(
allCryptoScopes(cryptos, field.cryptoScope),
allMachineScopes(machineList, field.machineScope)
)
return scopes.every(scope => {
const isAnyEnabled = () => _.some(refField => {
return isScopeEnabled(config, cryptos, machineList, refField, scope)
}, anyFields)
const areAllEnabled = () => _.every(refField => {
return isScopeEnabled(config, cryptos, machineList, refField, scope)
}, allFields)
const isBlank = _.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config))
const isRequired = (_.isEmpty(anyFields) || isAnyEnabled()) &&
(_.isEmpty(allFields) || areAllEnabled())
const hasDefault = !_.isNil(_.get('default', field))
const isValid = !isRequired || !isBlank || hasDefault
return isValid
})
}
function isScopeEnabled (config, cryptos, machineList, refField, scope) {
const [cryptoScope, machineScope] = scope
const candidateCryptoScopes = cryptoScope === 'global'
? allCryptoScopes(cryptos, refField.cryptoScope)
: [cryptoScope]
const candidateMachineScopes = machineScope === 'global'
? allMachineScopes(machineList, refField.machineScope)
: [ machineScope ]
const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes)
const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config)
const values = allRefCandidateScopes.map(getFallbackValue)
return values.some(r => r)
}
function getCryptos (config, machineList) {
const scopes = allScopes(['global'], allMachineScopes(machineList, 'both'))
const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config)
return scopes.reduce((acc, scope) => _.union(acc, scoped(scope)), [])
}
function getGroup (fieldCode) {
return _.find(group => _.includes(fieldCode, group.fields), schema.groups)
}
function getField (fieldCode) {
const group = getGroup(fieldCode)
return getGroupField(group, fieldCode)
}
function getGroupField (group, fieldCode) {
const field = _.find(_.matchesProperty('code', fieldCode), schema.fields)
return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field)
}
// Note: We can't use machine-loader because it relies on settings-loader,
// which relies on this
function getMachines () {
return db.any('select device_id from devices')
}
function fetchMachines () {
return getMachines()
.then(machineList => machineList.map(r => r.device_id))
}
function validateFieldParameter (value, validator) {
switch (validator.code) {
case 'required':
return true // We don't validate this here
case 'min':
return value >= validator.min
case 'max':
return value <= validator.max
default:
throw new Error('Unknown validation type: ' + validator.code)
}
}
function ensureConstraints (config) {
const pickField = fieldCode => schema.fields.find(r => r.code === fieldCode)
return Promise.resolve()
.then(() => {
config.every(fieldInstance => {
const fieldCode = fieldInstance.fieldLocator.code
if (_.includes(fieldCode, REMOVED_FIELDS)) return
const field = pickField(fieldCode)
if (!field) {
logger.warn('No such field: %s, %j', fieldCode, fieldInstance.fieldLocator.fieldScope)
return
}
const fieldValue = fieldInstance.fieldValue
const isValid = field.fieldValidation
.every(validator => validateFieldParameter(fieldValue.value, validator))
if (isValid) return true
throw new Error('Invalid config value')
})
})
}
function validateRequires (config) {
return fetchMachines()
.then(machineList => {
const cryptos = getCryptos(config, machineList)
return schema.groups.filter(group => {
return group.fields.some(fieldCode => {
const field = getGroupField(group, fieldCode)
if (!field.fieldValidation.find(r => r.code === 'required')) return false
const refFieldsAny = _.map(_.partial(getField, group), field.enabledIfAny)
const refFieldsAll = _.map(_.partial(getField, group), field.enabledIfAll)
const isInvalid = !satisfiesRequire(config, cryptos, machineList, field, refFieldsAny, refFieldsAll)
return isInvalid
})
})
})
.then(arr => arr.map(r => r.code))
}
function validate (config) {
return Promise.resolve()
.then(() => ensureConstraints(config))
.then(() => validateRequires(config))
.then(arr => {
if (arr.length === 0) return config
throw new Error('Invalid configuration:' + arr)
})
}
module.exports = {
SETTINGS_LOADER_SCHEMA_VERSION,
validate,
ensureConstraints,
validateRequires
}

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,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,15 +14,12 @@ 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 asyncLocalStorage.run(store, () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let count = 0 let count = 0
let handler let handler
@ -40,7 +35,7 @@ function run () {
.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)
@ -49,7 +44,6 @@ function run () {
handler = setInterval(runner, 10000) handler = setInterval(runner, 10000)
runner() runner()
}) })
})
} }
function loadSanctions (settings) { function loadSanctions (settings) {
@ -68,30 +62,27 @@ function loadSanctions (settings) {
}) })
} }
function startServer (settings) { async function startServer () {
return Promise.resolve() const app = await loadRoutes()
.then(() => {
poller.setup(['public']) poller.setup()
const httpsServerOptions = { const httpsServerOptions = {
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),
requestCert: true, requestCert: true,
rejectUnauthorized: false rejectUnauthorized: false
} }
const server = devMode const server = https.createServer(httpsServerOptions, app)
? http.createServer(routes.app)
: https.createServer(httpsServerOptions, routes.app)
const port = argv.port || 3000 const port = argv.port || 3000
if (devMode) logger.info('In dev mode') await new Promise((resolve) =>
server.listen({ port }, resolve),
server.listen(port, () => { )
logger.info('lamassu-server listening on port ' + logger.info(`lamassu-server listening on port ${port}`)
port + ' ' + (devMode ? '(http)' : '(https)'))
})
})
} }
module.exports = { run } module.exports = { run }

View file

@ -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 }

View file

@ -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: {

View file

@ -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()
} }

View file

@ -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

View file

@ -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',

View file

@ -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

View file

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

View file

@ -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
const store = defaultStore()
asyncLocalStorage.run(store, () => {
db.one(sql, [uuid.v4(), '', msgToSave, level, meta]) db.one(sql, [uuid.v4(), '', msgToSave, level, meta])
.then(_.mapKeys(_.camelCase)) .then(_.mapKeys(_.camelCase))
.catch(_.noop) .catch(_.noop)
}) })
})
module.exports = db module.exports = db

View file

@ -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]

View file

@ -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 }) => ({
typeDefs: require('./types'),
resolvers: require('./resolvers'),
context: ({ req, res }) => ({
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */ deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
deviceName: req.deviceName, /* lib/middlewares/authorize.js */ deviceName: req.deviceName, /* lib/middlewares/authorize.js */
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */ operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
pid: req.query.pid, pid: req.query.pid,
settings: req.settings, /* lib/middlewares/populateSettings.js */ settings: req.settings, /* lib/middlewares/populateSettings.js */
}), })
uploads: false,
playground: false, const graphQLServer = new ApolloServer({
typeDefs: require('./types'),
resolvers: require('./resolvers'),
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 }

View file

@ -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!

View file

@ -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
}) })

View file

@ -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)
}) })

View file

@ -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

View file

@ -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

View file

@ -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,6 +40,7 @@ if (!HOSTNAME) {
process.exit(1) process.exit(1)
} }
const loadRoutes = async () => {
const app = express() const app = express()
app.use(helmet()) app.use(helmet())
@ -47,38 +51,46 @@ 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)
// Dynamic import for graphql-upload since it's not a CommonJS module
const { default: graphqlUploadExpress } = await import('graphql-upload/graphqlUploadExpress.mjs')
const { default: GraphQLUpload } = await import('graphql-upload/GraphQLUpload.mjs')
app.use(graphqlUploadExpress()) app.use(graphqlUploadExpress())
const apolloServer = new ApolloServer({ const schema = makeExecutableSchema({
typeDefs, typeDefs,
resolvers, resolvers: mergeResolvers(resolvers, { Upload: GraphQLUpload }),
uploads: false, })
schemaDirectives: { const schemaWithDirectives = authDirectiveTransformer(schema)
auth: AuthDirective
}, const apolloServer = new ApolloServer({
playground: false, schema: schemaWithDirectives,
csrfPrevention: false,
introspection: false, introspection: false,
formatError: error => { formatError: (formattedError, error) => {
const exception = error?.extensions?.exception logger.error(error, JSON.stringify(error?.extensions || {}))
logger.error(error, JSON.stringify(exception || {})) return formattedError
return error
}, },
context: async (obj) => buildApolloContext(obj) plugins: [
devMode
? ApolloServerPluginLandingPageLocalDefault()
: ApolloServerPluginLandingPageDisabled()
]
}) })
apolloServer.applyMiddleware({ await apolloServer.start();
app,
cors: { app.use(
credentials: true, '/graphql',
origin: devMode && 'https://localhost:3001' express.json(),
} expressMiddleware(apolloServer, {
}) context: async ({ req, res }) => buildApolloContext({ req, res })
})
);
// cors on app for /api/register endpoint.
app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' }))
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false })) 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('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false }))
@ -87,14 +99,17 @@ app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
// Everything not on graphql or api/register is redirected to the front-end // 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'))) app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
const certOptions = { return app
key: fs.readFileSync(KEY_PATH),
cert: fs.readFileSync(CERT_PATH)
} }
function run () { const certOptions = {
const store = defaultStore() key: fs.readFileSync(KEY_PATH),
asyncLocalStorage.run(store, () => { cert: fs.readFileSync(CERT_PATH),
ca: fs.readFileSync(CA_PATH)
}
async function run () {
const app = await loadRoutes()
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}`
@ -104,7 +119,6 @@ function run () {
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 }

View file

@ -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 },

View file

@ -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

View file

@ -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 }

View file

@ -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) function authDirectiveTransformer(schema, directiveName = 'auth') {
type._requiredAuthRole = this.args.requires return mapSchema(schema, {
// 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
},
// For field definitions
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0]
if (directive) {
const requiredAuthRole = directive.requires
fieldConfig._requiredAuthRole = requiredAuthRole
} }
visitFieldDefinition (field, details) { // Get the parent object type
this.ensureFieldsWrapped(details.objectType) const objectType = schema.getType(typeName)
field._requiredAuthRole = this.args.requires
}
ensureFieldsWrapped (objectType) { // Apply auth check to the field's resolver
if (objectType._authFieldsWrapped) return const { resolve = defaultFieldResolver } = fieldConfig
objectType._authFieldsWrapped = true fieldConfig.resolve = function (root, args, context, info) {
const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole
const fields = objectType.getFields()
_.forEach(fieldName => {
const field = fields[fieldName]
const { resolve = defaultFieldResolver } = field
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

View file

@ -1,3 +1,3 @@
const AuthDirective = require('./auth') const authDirectiveTransformer = require('./auth')
module.exports = { AuthDirective } module.exports = { authDirectiveTransformer }

View 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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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 =>

View file

@ -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
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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
} }
` `

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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
} }

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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 }

View file

@ -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))
) )

View file

@ -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()

View file

@ -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([

View file

@ -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)`

View file

@ -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,

View file

@ -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
} }

View file

@ -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']

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 = {

View file

@ -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()
store.set('schema', schema)
// set asyncLocalStorage so settingsLoader loads settings for the right schema
return asyncLocalStorage.run(store, () => {
return settingsLoader.loadLatest() return settingsLoader.loadLatest()
.then(settings => { .then(settings => {
const pi = plugins(settings) const pi = plugins(settings)
cachedVariables.set(schema, { settings, pi, isReloading: false }) cachedVariables.set('public', { settings, pi, isReloading: false })
logger.debug(`Settings for schema '${schema}' reloaded in poller`) logger.debug(`Settings for schema 'public' reloaded in poller`)
return updateAndLoadSanctions() 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)
// }
function initializeEachSchema (schemas = ['public']) {
// for each schema set "thread variables" and do polling
return _.forEach(schema => {
const store = defaultStore()
store.set('schema', schema)
return asyncLocalStorage.run(store, () => {
return settingsLoader.loadLatest().then(settings => { 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) const pi = plugins(settings)
cachedVariables.set(schema, { settings, pi, isReloading: false }) cachedVariables.set('public', { settings, pi, isReloading: false })
schemaCallbacks.set(schema, []) return doPolling()
return doPolling(schema)
})
}).catch(console.error) }).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 }

View file

@ -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,8 +33,11 @@ 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 { expressMiddleware } = require('@apollo/server/express4')
const loadRoutes = async () => {
const app = express() const app = express()
const configRequiredRoutes = [ const configRequiredRoutes = [
@ -75,7 +76,6 @@ app.use('/', pairingRoutes)
app.use(findOperatorId) app.use(findOperatorId)
app.use(populateDeviceId) app.use(populateDeviceId)
app.use(computeSchema)
app.use(authorize) app.use(authorize)
app.use(configRequiredRoutes, populateSettings) app.use(configRequiredRoutes, populateSettings)
app.use(filterOldRequests) app.use(filterOldRequests)
@ -108,11 +108,21 @@ app.use('/units', unitsRoutes)
app.use('/probe', probeRoutes) app.use('/probe', probeRoutes)
graphQLServer.applyMiddleware({ app }) await graphQLServer.start()
app.use('/graphql',
express.json(),
expressMiddleware(graphQLServer, {
context,
}),
);
app.use(errorHandler) app.use(errorHandler)
app.use((req, res) => { app.use((req, res) => {
res.status(404).json({ error: 'No such route' }) res.status(404).json({ error: 'No such route' })
}) })
module.exports = { app } return app
}
module.exports = { loadRoutes }

View file

@ -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' }))

View file

@ -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)
}) })
} }

View file

@ -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()
.then(updateSql => {
const sql = [
'alter table devices add column name text',
updateSql,
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
.catch(() => {
const sql = [ const sql = [
'alter table devices add column name text', 'alter table devices add column name text',
'alter table devices alter column name set not null' 'alter table devices alter column name set not null'
] ]
return db.multi(sql, next) return db.multi(sql, next)
})
} }
exports.down = function (next) { exports.down = function (next) {

View file

@ -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) {

View 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()

View 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()

View 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()

View file

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

View file

@ -1 +0,0 @@
nodejs 22

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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>
)} )}

View file

@ -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} />}
/> />

View file

@ -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)

View file

@ -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 }) => {

View file

@ -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'
}) })

View file

@ -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}

View file

@ -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(

View file

@ -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)}>

View file

@ -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>

View file

@ -147,19 +147,25 @@ const Wizard = ({
onSubmit={onContinue} onSubmit={onContinue}
initialValues={stepOptions.initialValues} initialValues={stepOptions.initialValues}
validationSchema={stepOptions.schema}> validationSchema={stepOptions.schema}>
{({ errors }) => (
<Form className={classes.form}> <Form className={classes.form}>
<stepOptions.Component <stepOptions.Component
selectedValues={selectedValues} selectedValues={selectedValues}
customInfoRequirementOptions={customInfoRequirementOptions} customInfoRequirementOptions={customInfoRequirementOptions}
errors={errors}
{...stepOptions.props} {...stepOptions.props}
/> />
<div className={classes.submit}> <div className={classes.submit}>
{error && <ErrorMessage>Failed to save</ErrorMessage>} {error && <ErrorMessage>Failed to save</ErrorMessage>}
{Object.keys(errors).length > 0 && (
<ErrorMessage>{Object.values(errors)[0]}</ErrorMessage>
)}
<Button className={classes.button} type="submit"> <Button className={classes.button} type="submit">
{isLastStep ? 'Add Data' : 'Next'} {isLastStep ? 'Add Data' : 'Next'}
</Button> </Button>
</div> </div>
</Form> </Form>
)}
</Formik> </Formik>
</Modal> </Modal>
</> </>

View file

@ -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
} }

View file

@ -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