chore: server code formatting

This commit is contained in:
Rafael Taranto 2025-05-12 15:35:00 +01:00
parent aedabcbdee
commit 68517170e2
234 changed files with 9824 additions and 6195 deletions

View file

@ -19,8 +19,8 @@ const CA_PATH = process.env.CA_PATH
const version = require('../package.json').version
logger.info('Version: %s', version)
function run () {
return new Promise((resolve, reject) => {
function run() {
return new Promise(resolve => {
let count = 0
let handler
@ -31,12 +31,11 @@ function run () {
}
const runner = () => {
settingsLoader.loadLatest()
settingsLoader
.loadLatest()
.then(settings => {
clearInterval(handler)
return loadSanctions(settings)
.then(startServer)
.then(resolve)
return loadSanctions(settings).then(startServer).then(resolve)
})
.catch(errorHandler)
}
@ -46,23 +45,23 @@ function run () {
})
}
function loadSanctions (settings) {
return Promise.resolve()
.then(() => {
const triggers = configManager.getTriggers(settings.config)
const hasSanctions = complianceTriggers.hasSanctions(triggers)
function loadSanctions(settings) {
return Promise.resolve().then(() => {
const triggers = configManager.getTriggers(settings.config)
const hasSanctions = complianceTriggers.hasSanctions(triggers)
if (!hasSanctions) return
if (!hasSanctions) return
logger.info('Loading sanctions DB...')
return ofacUpdate.update()
.then(() => logger.info('Sanctions DB updated'))
.then(ofac.load)
.then(() => logger.info('Sanctions DB loaded'))
})
logger.info('Loading sanctions DB...')
return ofacUpdate
.update()
.then(() => logger.info('Sanctions DB updated'))
.then(ofac.load)
.then(() => logger.info('Sanctions DB loaded'))
})
}
async function startServer () {
async function startServer() {
const app = await loadRoutes()
poller.setup()
@ -72,16 +71,14 @@ async function startServer () {
cert: fs.readFileSync(CERT_PATH),
ca: fs.readFileSync(CA_PATH),
requestCert: true,
rejectUnauthorized: false
rejectUnauthorized: false,
}
const server = https.createServer(httpsServerOptions, app)
const port = argv.port || 3000
await new Promise((resolve) =>
server.listen({ port }, resolve),
)
await new Promise(resolve => server.listen({ port }, resolve))
logger.info(`lamassu-server listening on port ${port}`)
}

View file

@ -3,7 +3,7 @@ const crypto = require('crypto')
const constants = require('./constants')
const db = require('./db')
function createAuthToken (userID, type) {
function createAuthToken(userID, type) {
const token = crypto.randomBytes(32).toString('hex')
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '${constants.AUTH_TOKEN_EXPIRATION_TIME}' RETURNING *`
@ -11,5 +11,5 @@ function createAuthToken (userID, type) {
}
module.exports = {
createAuthToken
createAuthToken,
}

View file

@ -1,7 +1,6 @@
const _ = require('lodash/fp')
const sumService = require('@haensl/subset-sum')
const BN = require('./bn')
const logger = require('./logger')
const cc = require('./coin-change')
@ -11,7 +10,7 @@ const BILL_LIST_MODES = {
LOWEST_VALUE_FIRST: 2,
HIGHEST_VALUE_FIRST: 3,
UNIT_ROUND_ROBIN: 4,
VALUE_ROUND_ROBIN: 5
VALUE_ROUND_ROBIN: 5,
}
const buildBillList = (units, mode) => {
@ -23,7 +22,7 @@ const buildBillList = (units, mode) => {
return acc
},
[],
_.reverse(units)
_.reverse(units),
)
case BILL_LIST_MODES.FIRST_UNIT_FIRST:
return _.reduce(
@ -32,7 +31,7 @@ const buildBillList = (units, mode) => {
return acc
},
[],
units
units,
)
case BILL_LIST_MODES.LOWEST_VALUE_FIRST:
return _.reduce(
@ -41,7 +40,7 @@ const buildBillList = (units, mode) => {
return acc
},
[],
_.orderBy(['denomination'], ['asc'])(units)
_.orderBy(['denomination'], ['asc'])(units),
)
case BILL_LIST_MODES.HIGHEST_VALUE_FIRST:
return _.reduce(
@ -50,58 +49,59 @@ const buildBillList = (units, mode) => {
return acc
},
[],
_.orderBy(['denomination'], ['desc'])(units)
_.orderBy(['denomination'], ['desc'])(units),
)
case BILL_LIST_MODES.UNIT_ROUND_ROBIN:
{
const amountOfBills = _.reduce(
(acc, value) => acc + value.count,
0,
units
)
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
const bills = []
for(let i = 0; i < amountOfBills; i++) {
const idx = i % _.size(_units)
if (_units[idx].count > 0) {
bills.push(_units[idx].denomination)
_units[idx].count--
}
if (_units[idx].count === 0) {
_units.splice(idx, 1)
}
case BILL_LIST_MODES.UNIT_ROUND_ROBIN: {
const amountOfBills = _.reduce(
(acc, value) => acc + value.count,
0,
units,
)
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
const bills = []
for (let i = 0; i < amountOfBills; i++) {
const idx = i % _.size(_units)
if (_units[idx].count > 0) {
bills.push(_units[idx].denomination)
_units[idx].count--
}
return bills
if (_units[idx].count === 0) {
_units.splice(idx, 1)
}
}
case BILL_LIST_MODES.VALUE_ROUND_ROBIN:
{
const amountOfBills = _.reduce(
(acc, value) => acc + value.count,
0,
units
)
const _units = _.flow([_.filter(it => it.count > 0), _.orderBy(['denomination'], ['asc'])])(_.cloneDeep(units))
const bills = []
for(let i = 0; i < amountOfBills; i++) {
const idx = i % _.size(_units)
if (_units[idx].count > 0) {
bills.push(_units[idx].denomination)
_units[idx].count--
}
if (_units[idx].count === 0) {
_units.splice(idx, 1)
}
return bills
}
case BILL_LIST_MODES.VALUE_ROUND_ROBIN: {
const amountOfBills = _.reduce(
(acc, value) => acc + value.count,
0,
units,
)
const _units = _.flow([
_.filter(it => it.count > 0),
_.orderBy(['denomination'], ['asc']),
])(_.cloneDeep(units))
const bills = []
for (let i = 0; i < amountOfBills; i++) {
const idx = i % _.size(_units)
if (_units[idx].count > 0) {
bills.push(_units[idx].denomination)
_units[idx].count--
}
return bills
if (_units[idx].count === 0) {
_units.splice(idx, 1)
}
}
return bills
}
default:
throw new Error(`Invalid mode: ${mode}`)
}
@ -113,11 +113,13 @@ const getSolution_old = (units, amount, mode) => {
if (_.sum(billList) < amount.toNumber()) {
return []
}
const solver = sumService.subsetSum(billList, amount.toNumber())
const solution = _.countBy(Math.floor, solver.next().value)
return Object.entries(solution)
.map(([denomination, provisioned]) => [_.toNumber(denomination), provisioned])
return Object.entries(solution).map(([denomination, provisioned]) => [
_.toNumber(denomination),
provisioned,
])
}
const getSolution = (units, amount) => {
@ -128,28 +130,35 @@ const getSolution = (units, amount) => {
}
const solutionToOriginalUnits = (solution, units) => {
const billsToAssign = (count, left) => _.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left)
const billsToAssign = (count, left) =>
_.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left)
const billsLeft = Object.fromEntries(solution)
return units.map(
({ count, name, denomination }) => {
const provisioned = billsToAssign(count, billsLeft[denomination])
billsLeft[denomination] -= provisioned
return { name, denomination, provisioned }
}
)
return units.map(({ count, name, denomination }) => {
const provisioned = billsToAssign(count, billsLeft[denomination])
billsLeft[denomination] -= provisioned
return { name, denomination, provisioned }
})
}
function makeChange(outCassettes, amount) {
const ss_solution = getSolution_old(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN)
const ss_solution = getSolution_old(
outCassettes,
amount,
BILL_LIST_MODES.VALUE_ROUND_ROBIN,
)
const cc_solution = getSolution(outCassettes, amount)
if (!cc.check(cc_solution, amount.toNumber())) {
logger.error(new Error("coin-change provided a bad solution"))
logger.error(new Error('coin-change provided a bad solution'))
return solutionToOriginalUnits(ss_solution, outCassettes)
}
if (!!ss_solution !== !!cc_solution) {
logger.error(new Error(`subset-sum and coin-change don't agree on solvability -- subset-sum:${!!ss_solution} coin-change:${!!cc_solution}`))
logger.error(
new Error(
`subset-sum and coin-change don't agree on solvability -- subset-sum:${!!ss_solution} coin-change:${!!cc_solution}`,
),
)
return solutionToOriginalUnits(ss_solution, outCassettes)
}

View file

@ -8,7 +8,7 @@ const getBlacklist = () =>
db.any(
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
FROM blacklist JOIN blacklist_messages
ON blacklist.blacklist_message_id = blacklist_messages.id`
ON blacklist.blacklist_message_id = blacklist_messages.id`,
)
const deleteFromBlacklist = address => {
@ -19,7 +19,9 @@ const deleteFromBlacklist = address => {
const isValidAddress = address => {
try {
return !_.isEmpty(addressDetector.getSupportedCoinsForAddress(address).matches)
return !_.isEmpty(
addressDetector.getSupportedCoinsForAddress(address).matches,
)
} catch {
return false
}
@ -29,24 +31,20 @@ const insertIntoBlacklist = address => {
if (!isValidAddress(address)) {
return Promise.reject(new Error('Invalid address'))
}
return db
.none(
'INSERT INTO blacklist (address) VALUES ($1);',
[address]
)
return db.none('INSERT INTO blacklist (address) VALUES ($1);', [address])
}
function blocked (address) {
function blocked(address) {
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
return db.oneOrNone(sql, [address])
}
function getMessages () {
function getMessages() {
const sql = `SELECT * FROM blacklist_messages`
return db.any(sql)
}
function editBlacklistMessage (id, content) {
function editBlacklistMessage(id, content) {
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
return db.oneOrNone(sql, [content, id])
}
@ -57,5 +55,5 @@ module.exports = {
deleteFromBlacklist,
insertIntoBlacklist,
getMessages,
editBlacklistMessage
editBlacklistMessage,
}

View file

@ -1,5 +1,4 @@
const path = require('path')
const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
@ -13,9 +12,11 @@ const coinRec = coinUtils.getCryptoCurrency('BTC')
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp') : '/tmp'
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
const usrBinDir = isDevMode()
? path.resolve(BLOCKCHAIN_DIR, 'bin')
: '/usr/local/bin'
function setup (dataDir) {
function setup(dataDir) {
!isDevMode() && common.firewall([coinRec.defaultPort])
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
@ -23,12 +24,17 @@ function setup (dataDir) {
!isDevMode() && common.writeSupervisorConfig(coinRec, cmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating Bitcoin Core. This may take a minute...')
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Bitcoin Core: Package signature do not match!')
if (
common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Bitcoin Core: Package signature do not match!',
)
return
}
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
@ -38,39 +44,71 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`rm -r ${tmpDir}/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm ${tmpDir}/bitcoin.tar.gz`)
if (common.es(`grep "addresstype=p2sh-segwit" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
if (
common.es(
`grep "addresstype=p2sh-segwit" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`,
)
) {
common.logger.info(`Enabling bech32 receiving addresses in config file..`)
common.es(`sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
common.es(
`sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`,
)
} else {
common.logger.info(`bech32 receiving addresses already defined, skipping...`)
common.logger.info(
`bech32 receiving addresses already defined, skipping...`,
)
}
if (common.es(`grep "changetype=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
if (
common.es(
`grep "changetype=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`,
)
) {
common.logger.info(`changetype already defined, skipping...`)
} else {
common.logger.info(`Enabling bech32 change addresses in config file..`)
common.es(`echo "\nchangetype=bech32" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
common.es(
`echo "\nchangetype=bech32" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`,
)
}
if (common.es(`grep "listenonion=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
if (
common.es(
`grep "listenonion=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`,
)
) {
common.logger.info(`listenonion already defined, skipping...`)
} else {
common.logger.info(`Setting 'listenonion=0' in config file...`)
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
common.es(
`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`,
)
}
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
if (
common.es(
`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`,
)
) {
common.logger.info(`fallbackfee already defined, skipping...`)
} else {
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
common.es(
`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`,
)
}
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
if (
common.es(
`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`,
)
) {
common.logger.info(`rpcworkqueue already defined, skipping...`)
} else {
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
common.es(
`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`,
)
}
if (isCurrentlyRunning && !isDevMode()) {
@ -81,7 +119,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Bitcoin Core is updated!')
}
function buildConfig () {
function buildConfig() {
return `rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
${isDevMode() ? `regtest=1` : ``}
@ -97,13 +135,15 @@ walletrbf=1
listenonion=0
fallbackfee=0.00005
rpcworkqueue=2000
${isDevMode()
? `[regtest]
${
isDevMode()
? `[regtest]
rpcport=18333
bind=0.0.0.0:18332
${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`
: `rpcport=8333
: `rpcport=8333
bind=0.0.0.0:8332
${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`}
${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`
}
`
}

View file

@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
const coinRec = coinUtils.getCryptoCurrency('BCH')
function setup (dataDir) {
function setup(dataDir) {
common.firewall([coinRec.defaultPort])
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
@ -16,12 +16,17 @@ function setup (dataDir) {
common.writeSupervisorConfig(coinRec, cmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
common.es(`sudo supervisorctl stop bitcoincash`)
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!')
if (
common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Bitcoin Cash: Package signature do not match!',
)
return
}
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
@ -32,11 +37,17 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/bitcoincash.tar.gz`)
if (common.es(`grep "listenonion=" /mnt/blockchains/bitcoincash/bitcoincash.conf || true`)) {
if (
common.es(
`grep "listenonion=" /mnt/blockchains/bitcoincash/bitcoincash.conf || true`,
)
) {
common.logger.info(`listenonion already defined, skipping...`)
} else {
common.logger.info(`Setting 'listenonion=0' in config file...`)
common.es(`echo "\nlistenonion=0" >> /mnt/blockchains/bitcoincash/bitcoincash.conf`)
common.es(
`echo "\nlistenonion=0" >> /mnt/blockchains/bitcoincash/bitcoincash.conf`,
)
}
if (isCurrentlyRunning) {
@ -47,7 +58,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Bitcoin Cash is updated!')
}
function buildConfig () {
function buildConfig() {
return `rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
dbcache=500

View file

@ -7,7 +7,7 @@ const makeDir = require('make-dir')
const _ = require('lodash/fp')
const logger = require('console-log-level')({level: 'info'})
const logger = require('console-log-level')({ level: 'info' })
const { isDevMode } = require('../environment-helper')
@ -23,13 +23,15 @@ module.exports = {
isInstalledSoftware,
writeFile,
getBinaries,
isUpdateDependent
isUpdateDependent,
}
const BINARIES = {
BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
defaultUrl:
'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash:
'376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-28.0/bin',
@ -46,16 +48,20 @@ const BINARIES = {
urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d',
},
DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
defaultUrl:
'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultUrlHash:
'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
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',
dir: 'dashcore-21.1.1/bin',
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
},
LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
defaultUrl:
'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash:
'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
defaultDir: 'litecoin-0.18.1/bin',
url: 'https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz',
dir: 'litecoin-0.21.4/bin',
@ -64,38 +70,44 @@ const BINARIES = {
BCH: {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.0/bitcoin-cash-node-28.0.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-28.0.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']],
files: [
['bitcoind', 'bitcoincashd'],
['bitcoin-cli', 'bitcoincash-cli'],
],
urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158',
},
XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.3.4',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']],
files: [
['monerod', 'monerod'],
['monero-wallet-rpc', 'monero-wallet-rpc'],
],
urlHash: '51ba03928d189c1c11b5379cab17dd9ae8d2230056dc05c872d0f8dba4a87f1d',
}
},
}
const coinsUpdateDependent = ['BTC', 'LTC', 'DASH']
function firewall (ports) {
function firewall(ports) {
if (!ports || ports.length === 0) throw new Error('No ports supplied')
const portsString = ports.join(',')
es(`sudo ufw allow ${portsString}`)
}
function randomPass () {
function randomPass() {
return crypto.randomBytes(32).toString('hex')
}
function es (cmd) {
const env = {HOME: os.userInfo().homedir}
const options = {encoding: 'utf8', env}
function es(cmd) {
const env = { HOME: os.userInfo().homedir }
const options = { encoding: 'utf8', env }
const res = cp.execSync(cmd, options)
logger.debug(res)
return res.toString()
}
function generateSupervisorConfig (cryptoCode, command, isWallet = false) {
function generateSupervisorConfig(cryptoCode, command, isWallet = false) {
return `[program:${cryptoCode}${isWallet ? `-wallet` : ``}]
command=nice ${command}
autostart=true
@ -108,34 +120,46 @@ environment=HOME="/root"
`
}
function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
function writeSupervisorConfig(coinRec, cmd, walletCmd = '') {
if (isInstalledSoftware(coinRec)) return
const blockchain = coinRec.code
if (!_.isNil(coinRec.wallet)) {
const supervisorConfigWallet = generateSupervisorConfig(blockchain, walletCmd, true)
writeFile(`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`, supervisorConfigWallet)
const supervisorConfigWallet = generateSupervisorConfig(
blockchain,
walletCmd,
true,
)
writeFile(
`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`,
supervisorConfigWallet,
)
}
const supervisorConfig = generateSupervisorConfig(blockchain, cmd)
writeFile(`/etc/supervisor/conf.d/${coinRec.code}.conf`, supervisorConfig)
}
function isInstalledSoftware (coinRec) {
function isInstalledSoftware(coinRec) {
if (isDevMode()) {
return fs.existsSync(`${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`)
&& fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`)
return (
fs.existsSync(
`${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`,
) && fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`)
)
}
const nodeInstalled = fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`)
const nodeInstalled = fs.existsSync(
`/etc/supervisor/conf.d/${coinRec.code}.conf`,
)
const walletInstalled = _.isNil(coinRec.wallet)
? true
: fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.wallet.conf`)
return nodeInstalled && walletInstalled
}
function fetchAndInstall (coinRec) {
function fetchAndInstall(coinRec) {
const requiresUpdate = isUpdateDependent(coinRec.cryptoCode)
if (isInstalledSoftware(coinRec)) return
@ -149,12 +173,16 @@ function fetchAndInstall (coinRec) {
es(`wget -q ${url}`)
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`)
logger.info(
`Failed to install ${coinRec.code}: Package signature do not match!`,
)
return
}
es(`tar -xf ${downloadFile}`)
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
const usrBinDir = isDevMode()
? path.resolve(BLOCKCHAIN_DIR, 'bin')
: '/usr/local/bin'
if (isDevMode()) {
makeDir.sync(usrBinDir)
@ -170,7 +198,7 @@ function fetchAndInstall (coinRec) {
}, binaries.files)
}
function writeFile (path, content) {
function writeFile(path, content) {
try {
fs.writeFileSync(path, content)
} catch (err) {
@ -183,12 +211,12 @@ function writeFile (path, content) {
}
}
function getBinaries (coinCode) {
function getBinaries(coinCode) {
const binaries = BINARIES[coinCode]
if (!binaries) throw new Error(`No such coin: ${coinCode}`)
return binaries
}
function isUpdateDependent (coinCode) {
function isUpdateDependent(coinCode) {
return _.includes(coinCode, coinsUpdateDependent)
}

View file

@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
const coinRec = coinUtils.getCryptoCurrency('DASH')
function setup (dataDir) {
function setup(dataDir) {
common.firewall([coinRec.defaultPort])
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
@ -16,12 +16,17 @@ function setup (dataDir) {
common.writeSupervisorConfig(coinRec, cmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating Dash Core. This may take a minute...')
common.es(`sudo supervisorctl stop dash`)
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Dash Core: Package signature do not match!')
if (
common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Dash Core: Package signature do not match!',
)
return
}
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
@ -31,20 +36,38 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/dash.tar.gz`)
if (common.es(`grep "enableprivatesend=" /mnt/blockchains/dash/dash.conf || true`)) {
if (
common.es(
`grep "enableprivatesend=" /mnt/blockchains/dash/dash.conf || true`,
)
) {
common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`)
common.es(`sed -i 's/enableprivatesend/enablecoinjoin/g' /mnt/blockchains/dash/dash.conf`)
} else if (common.es(`grep "enablecoinjoin=" /mnt/blockchains/dash/dash.conf || true`)) {
common.es(
`sed -i 's/enableprivatesend/enablecoinjoin/g' /mnt/blockchains/dash/dash.conf`,
)
} else if (
common.es(`grep "enablecoinjoin=" /mnt/blockchains/dash/dash.conf || true`)
) {
common.logger.info(`enablecoinjoin already defined, skipping...`)
} else {
common.logger.info(`Enabling CoinJoin in config file...`)
common.es(`echo "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`)
}
if (common.es(`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`)) {
if (
common.es(
`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`,
)
) {
common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`)
common.es(`sed -i 's/privatesendautostart/coinjoinautostart/g' /mnt/blockchains/dash/dash.conf`)
} else if (common.es(`grep "coinjoinautostart=" /mnt/blockchains/dash/dash.conf || true`)) {
common.es(
`sed -i 's/privatesendautostart/coinjoinautostart/g' /mnt/blockchains/dash/dash.conf`,
)
} else if (
common.es(
`grep "coinjoinautostart=" /mnt/blockchains/dash/dash.conf || true`,
)
) {
common.logger.info(`coinjoinautostart already defined, skipping...`)
} else {
common.logger.info(`Enabling CoinJoin AutoStart in config file...`)
@ -53,7 +76,9 @@ function updateCore (coinRec, isCurrentlyRunning) {
if (common.es(`grep "litemode=" /mnt/blockchains/dash/dash.conf || true`)) {
common.logger.info(`Switching from 'LiteMode' to 'DisableGovernance'...`)
common.es(`sed -i 's/litemode/disablegovernance/g' /mnt/blockchains/dash/dash.conf`)
common.es(
`sed -i 's/litemode/disablegovernance/g' /mnt/blockchains/dash/dash.conf`,
)
} else {
common.es(`echo "\ndisablegovernance already defined, skipping..."`)
}
@ -66,7 +91,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Dash Core is updated!')
}
function buildConfig () {
function buildConfig() {
return `rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
dbcache=500

View file

@ -6,20 +6,20 @@ const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
const MOUNT_POINT = BLOCKCHAIN_DIR
module.exports = {prepareVolume}
module.exports = { prepareVolume }
const logger = common.logger
function isMounted () {
function isMounted() {
return fs.existsSync(MOUNT_POINT)
}
function isFormatted (volumePath) {
function isFormatted(volumePath) {
const res = common.es(`file --dereference -s ${volumePath}`).trim()
return res !== `${volumePath}: data`
}
function formatVolume (volumePath) {
function formatVolume(volumePath) {
if (isFormatted(volumePath)) {
logger.info('Volume is already formatted.')
return
@ -29,7 +29,7 @@ function formatVolume (volumePath) {
common.es(`sudo mkfs.ext4 ${volumePath}`)
}
function mountVolume (volumePath) {
function mountVolume(volumePath) {
if (isMounted()) {
logger.info('Volume is already mounted.')
return
@ -38,10 +38,12 @@ function mountVolume (volumePath) {
logger.info('Mounting...')
common.es(`sudo mkdir -p ${MOUNT_POINT}`)
common.es(`sudo mount -o discard,defaults ${volumePath} ${MOUNT_POINT}`)
common.es(`echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`)
common.es(
`echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`,
)
}
function locateVolume () {
function locateVolume() {
const res = common.es('ls /dev/disk/by-id/*')
const lines = res.trim().split('\n')
@ -58,7 +60,7 @@ function locateVolume () {
return lines[0].trim()
}
function prepareVolume () {
function prepareVolume() {
if (isMounted()) {
logger.info('Volume is already mounted.')
return true

View file

@ -4,11 +4,16 @@ const common = require('./common')
module.exports = { setup, updateCore }
function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...')
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info(
'Updating the Geth Ethereum wallet. This may take a minute...',
)
common.es(`sudo supervisorctl stop ethereum`)
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
if (
common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info('Failed to update Geth: Package signature do not match!')
return
}
@ -27,7 +32,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Geth is updated!')
}
function setup (dataDir) {
function setup(dataDir) {
const coinRec = coinUtils.getCryptoCurrency('ETH')
common.firewall([coinRec.defaultPort])
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --http`

View file

@ -10,7 +10,11 @@ const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const settingsLoader = require('../new-settings-loader')
const wallet = require('../wallet')
const { isDevMode, isRemoteNode, isRemoteWallet } = require('../environment-helper')
const {
isDevMode,
isRemoteNode,
isRemoteWallet,
} = require('../environment-helper')
const common = require('./common')
const doVolume = require('./do-volume')
@ -24,35 +28,38 @@ const PLUGINS = {
BCH: require('./bitcoincash.js'),
DASH: require('./dash.js'),
LTC: require('./litecoin.js'),
XMR: require('./monero.js')
XMR: require('./monero.js'),
}
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
module.exports = {
isEnvironmentValid,
run
run,
}
function installedVolumeFilePath (crypto) {
function installedVolumeFilePath(crypto) {
return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed')
}
function isInstalledVolume (crypto) {
function isInstalledVolume(crypto) {
return fs.existsSync(installedVolumeFilePath(crypto))
}
function isInstalledSoftware (crypto) {
function isInstalledSoftware(crypto) {
return common.isInstalledSoftware(crypto)
}
function processCryptos (codes) {
function processCryptos(codes) {
if (_.isEmpty(codes)) {
logger.info('No cryptos selected. Exiting.')
process.exit(0)
}
logger.info('Thanks! Installing: %s. Will take a while...', _.join(', ', codes))
logger.info(
'Thanks! Installing: %s. Will take a while...',
_.join(', ', codes),
)
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
@ -89,22 +96,32 @@ function processCryptos (codes) {
logger.info('Installation complete.')
}
function isEnvironmentValid (crypto) {
function isEnvironmentValid(crypto) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_LOCATION`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_LOCATION is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_LOCATION is not set!`,
)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_WALLET_LOCATION`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_WALLET_LOCATION is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_WALLET_LOCATION is not set!`,
)
if (isRemoteWallet(crypto) && !isRemoteNode(crypto))
throw new Error(`Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!`)
throw new Error(
`Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!`,
)
if (isRemoteNode(crypto) && !isRemoteWallet(crypto)) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_HOST`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_HOST is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_HOST is not set!`,
)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PORT`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PORT is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_PORT is not set!`,
)
if (_.isEmpty(process.env.BLOCKCHAIN_DIR))
throw new Error(`The environment variable for BLOCKCHAIN_DIR is not set!`)
@ -112,28 +129,39 @@ function isEnvironmentValid (crypto) {
if (isRemoteWallet(crypto)) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_HOST`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_HOST is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_RPC_HOST is not set!`,
)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_PORT`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_PORT is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_RPC_PORT is not set!`,
)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_USER`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_USER is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_USER is not set!`,
)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PASSWORD`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PASSWORD is not set!`)
throw new Error(
`The environment variable for ${crypto.cryptoCode}_NODE_PASSWORD is not set!`,
)
}
return true
}
function setupCrypto (crypto) {
function setupCrypto(crypto) {
logger.info(`Installing ${crypto.display}...`)
if (!isEnvironmentValid(crypto)) throw new Error(`Environment error for ${crypto.display}`)
if (!isEnvironmentValid(crypto))
throw new Error(`Environment error for ${crypto.display}`)
if (isRemoteWallet(crypto)) {
logger.info(`Environment variable ${crypto.cryptoCode}_WALLET_LOCATION is set as 'remote', so there's no need to install a node in the system. Exiting...`)
logger.info(
`Environment variable ${crypto.cryptoCode}_WALLET_LOCATION is set as 'remote', so there's no need to install a node in the system. Exiting...`,
)
return
}
@ -141,7 +169,9 @@ function setupCrypto (crypto) {
makeDir.sync(cryptoDir)
const cryptoPlugin = plugin(crypto)
const oldDir = process.cwd()
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp', 'blockchain-install') : '/tmp/blockchain-install'
const tmpDir = isDevMode()
? path.resolve(BLOCKCHAIN_DIR, 'tmp', 'blockchain-install')
: '/tmp/blockchain-install'
makeDir.sync(tmpDir)
process.chdir(tmpDir)
@ -157,62 +187,74 @@ function setupCrypto (crypto) {
process.chdir(oldDir)
}
function updateCrypto (crypto) {
function updateCrypto(crypto) {
if (!common.isUpdateDependent(crypto.cryptoCode)) return
const cryptoPlugin = plugin(crypto)
// TODO: we need to refactor the way we retrieve this status, p.e Monero uses two
// services with specific names, so each coin should have its implementation.
// Currently, it's not a breaking change because only BTC is update dependent
const status = common.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`).trim()
const status = common
.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`)
.trim()
const isCurrentlyRunning = _.includes(status, ['RUNNING', 'STARTING'])
cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), isCurrentlyRunning)
cryptoPlugin.updateCore(
common.getBinaries(crypto.cryptoCode),
isCurrentlyRunning,
)
}
function plugin (crypto) {
function plugin(crypto) {
const plugin = PLUGINS[crypto.cryptoCode]
if (!plugin) throw new Error(`No such plugin: ${crypto.cryptoCode}`)
return plugin
}
function getBlockchainSyncStatus (cryptoList) {
return settingsLoader.loadLatest()
.then(settings => {
if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready')
function getBlockchainSyncStatus(cryptoList) {
return settingsLoader.loadLatest().then(settings => {
if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready')
const blockchainStatuses = _.reduce((acc, value) => {
const processStatus = common.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`).trim()
return acc.then(a => {
if (processStatus === 'RUNNING') {
return wallet.checkBlockchainStatus(settings, value.cryptoCode)
.then(res => Promise.resolve({ ...a, [value.cryptoCode]: res }))
}
return Promise.resolve({ ...a })
})
},
Promise.resolve({}),
cryptoList
)
const blockchainStatuses = _.reduce(
(acc, value) => {
const processStatus = common
.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`)
.trim()
return acc.then(a => {
if (processStatus === 'RUNNING') {
return wallet
.checkBlockchainStatus(settings, value.cryptoCode)
.then(res => Promise.resolve({ ...a, [value.cryptoCode]: res }))
}
return Promise.resolve({ ...a })
})
},
Promise.resolve({}),
cryptoList,
)
return blockchainStatuses
})
return blockchainStatuses
})
}
function isInstalled (crypto) {
function isInstalled(crypto) {
return isDevMode()
? isInstalledSoftware(crypto)
: isInstalledSoftware(crypto) && isInstalledVolume(crypto)
}
function isDisabled (crypto) {
function isDisabled(crypto) {
switch (crypto.cryptoCode) {
case 'XMR':
return isInstalled(crypto) && 'Installed' || isInstalled(_.find(it => it.code === 'zcash', cryptos)) && 'Insufficient resources. Contact support.'
return (
(isInstalled(crypto) && 'Installed') ||
(isInstalled(_.find(it => it.code === 'zcash', cryptos)) &&
'Insufficient resources. Contact support.')
)
default:
return isInstalled(crypto) && 'Installed'
}
}
function run () {
function run() {
const choices = _.flow([
_.filter(c => !c.hideFromInstall),
_.map(c => {
@ -220,47 +262,70 @@ function run () {
name: c.display,
value: c.code,
checked: isInstalled(c),
disabled: isDisabled(c)
disabled: isDisabled(c),
}
}),
])(cryptos)
const questions = []
const validateAnswers = async (answers) => {
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
const validateAnswers = async answers => {
if (_.size(answers) > 2)
return {
message: `Please insert a maximum of two coins to install.`,
isValid: false,
}
if (
_.isEmpty(_.difference(['monero', 'zcash'], answers)) ||
(_.includes('monero', answers) && isInstalled(_.find(it => it.code === 'zcash', cryptos))) ||
(_.includes('zcash', answers) && isInstalled(_.find(it => it.code === 'monero', cryptos)))
(_.includes('monero', answers) &&
isInstalled(_.find(it => it.code === 'zcash', cryptos))) ||
(_.includes('zcash', answers) &&
isInstalled(_.find(it => it.code === 'monero', cryptos)))
) {
return { message: `Zcash and Monero installations are temporarily mutually exclusive, given the space needed for their blockchains. Contact support for more information.`, isValid: false }
return {
message: `Zcash and Monero installations are temporarily mutually exclusive, given the space needed for their blockchains. Contact support for more information.`,
isValid: false,
}
}
return getBlockchainSyncStatus(cryptos)
.then(blockchainStatuses => {
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
if (_.size(answers) + result.syncing > 2) {
return { message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, isValid: false }
return getBlockchainSyncStatus(cryptos).then(blockchainStatuses => {
const result = _.reduce(
(acc, value) => ({
...acc,
[value]: _.isNil(acc[value]) ? 1 : acc[value] + 1,
}),
{},
_.values(blockchainStatuses),
)
if (_.size(answers) + result.syncing > 2) {
return {
message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`,
isValid: false,
}
}
if (result.syncing > 2) {
return { message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`, isValid: false }
if (result.syncing > 2) {
return {
message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`,
isValid: false,
}
}
return { message: null, isValid: true }
})
return { message: null, isValid: true }
})
}
questions.push({
type: 'checkbox',
name: 'crypto',
message: 'Which cryptocurrencies would you like to install?\nTo prevent server resource overloading, only TWO coins should be syncing simultaneously.\nMore coins can be installed after this process is over.',
choices
message:
'Which cryptocurrencies would you like to install?\nTo prevent server resource overloading, only TWO coins should be syncing simultaneously.\nMore coins can be installed after this process is over.',
choices,
})
inquirer.prompt(questions)
inquirer
.prompt(questions)
.then(answers => Promise.all([validateAnswers(answers.crypto), answers]))
.then(([res, answers]) => {
if (res.isValid) {

View file

@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
const coinRec = coinUtils.getCryptoCurrency('LTC')
function setup (dataDir) {
function setup(dataDir) {
common.firewall([coinRec.defaultPort])
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
@ -16,12 +16,17 @@ function setup (dataDir) {
common.writeSupervisorConfig(coinRec, cmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating Litecoin Core. This may take a minute...')
common.es(`sudo supervisorctl stop litecoin`)
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Litecoin Core: Package signature do not match!')
if (
common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Litecoin Core: Package signature do not match!',
)
return
}
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
@ -31,25 +36,43 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/litecoin.tar.gz`)
if (common.es(`grep "changetype=" /mnt/blockchains/litecoin/litecoin.conf || true`)) {
if (
common.es(
`grep "changetype=" /mnt/blockchains/litecoin/litecoin.conf || true`,
)
) {
common.logger.info(`changetype already defined, skipping...`)
} else {
common.logger.info(`Enabling bech32 change addresses in config file..`)
common.es(`echo "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`)
common.es(
`echo "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`,
)
}
if (common.es(`grep "blockfilterindex=" /mnt/blockchains/litecoin/litecoin.conf || true`)) {
if (
common.es(
`grep "blockfilterindex=" /mnt/blockchains/litecoin/litecoin.conf || true`,
)
) {
common.logger.info(`blockfilterindex already defined, skipping...`)
} else {
common.logger.info(`Disabling blockfilterindex in config file..`)
common.es(`echo "\nblockfilterindex=0" >> /mnt/blockchains/litecoin/litecoin.conf`)
common.es(
`echo "\nblockfilterindex=0" >> /mnt/blockchains/litecoin/litecoin.conf`,
)
}
if (common.es(`grep "peerblockfilters=" /mnt/blockchains/litecoin/litecoin.conf || true`)) {
if (
common.es(
`grep "peerblockfilters=" /mnt/blockchains/litecoin/litecoin.conf || true`,
)
) {
common.logger.info(`peerblockfilters already defined, skipping...`)
} else {
common.logger.info(`Disabling peerblockfilters in config file..`)
common.es(`echo "\npeerblockfilters=0" >> /mnt/blockchains/litecoin/litecoin.conf`)
common.es(
`echo "\npeerblockfilters=0" >> /mnt/blockchains/litecoin/litecoin.conf`,
)
}
if (isCurrentlyRunning) {
@ -60,7 +83,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Litecoin Core is updated!')
}
function buildConfig () {
function buildConfig() {
return `rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
dbcache=500

View file

@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
const coinRec = utils.getCryptoCurrency('XMR')
function setup (dataDir) {
function setup(dataDir) {
common.firewall([coinRec.defaultPort])
const auth = `lamassuserver:${common.randomPass()}`
const config = buildConfig(auth)
@ -18,19 +18,26 @@ function setup (dataDir) {
common.writeSupervisorConfig(coinRec, cmd, walletCmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating Monero. This may take a minute...')
common.es(`sudo supervisorctl stop monero monero-wallet`)
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Monero: Package signature do not match!')
if (
common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Monero: Package signature do not match!',
)
return
}
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...')
common.es(`cp /tmp/${coinRec.dir}/monerod /usr/local/bin/monerod`)
common.es(`cp /tmp/${coinRec.dir}/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc`)
common.es(
`cp /tmp/${coinRec.dir}/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc`,
)
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/monero.tar.gz`)
@ -42,7 +49,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Monero is updated!')
}
function buildConfig (auth) {
function buildConfig(auth) {
return `rpc-login=${auth}
stagenet=0
restricted-rpc=1

View file

@ -9,12 +9,17 @@ module.exports = { setup, updateCore }
const es = common.es
const logger = common.logger
function updateCore (coinRec, isCurrentlyRunning) {
function updateCore(coinRec, isCurrentlyRunning) {
common.logger.info('Updating your Zcash wallet. This may take a minute...')
common.es(`sudo supervisorctl stop zcash`)
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Zcash: Package signature do not match!')
if (
common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !==
coinRec.urlHash
) {
common.logger.info(
'Failed to update Zcash: Package signature do not match!',
)
return
}
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
@ -24,11 +29,17 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/zcash.tar.gz`)
if (common.es(`grep "walletrequirebackup=" /mnt/blockchains/zcash/zcash.conf || true`)) {
if (
common.es(
`grep "walletrequirebackup=" /mnt/blockchains/zcash/zcash.conf || true`,
)
) {
common.logger.info(`walletrequirebackup already defined, skipping...`)
} else {
common.logger.info(`Setting 'walletrequirebackup=false' in config file...`)
common.es(`echo "\nwalletrequirebackup=false" >> /mnt/blockchains/zcash/zcash.conf`)
common.es(
`echo "\nwalletrequirebackup=false" >> /mnt/blockchains/zcash/zcash.conf`,
)
}
if (isCurrentlyRunning) {
@ -39,7 +50,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Zcash is updated!')
}
function setup (dataDir) {
function setup(dataDir) {
es('sudo apt-get update')
es('sudo apt-get install libgomp1 -y')
const coinRec = coinUtils.getCryptoCurrency('ZEC')
@ -54,7 +65,7 @@ function setup (dataDir) {
common.writeSupervisorConfig(coinRec, cmd)
}
function buildConfig () {
function buildConfig() {
return `mainnet=1
addnode=mainnet.z.cash
rpcuser=lamassuserver

View file

@ -1,16 +1,18 @@
const axios = require("axios");
const axios = require('axios')
const getSatBEstimateFee = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
return axios
.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data.hourFee)
}
const getSatBEstimateFees = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
return axios
.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data)
}
module.exports = {
getSatBEstimateFees,
getSatBEstimateFee
}
getSatBEstimateFee,
}

View file

@ -8,63 +8,73 @@ const cashInLow = require('./cash-in-low')
module.exports = { atomic }
function atomic (machineTx, pi) {
function atomic(machineTx) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
function transaction (t) {
function transaction(t) {
const sql = 'select * from cash_in_txs where id=$1'
const sql2 = 'select * from bills where cash_in_txs_id=$1'
return t.oneOrNone(sql, [machineTx.id])
.then(row => {
if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError({ txId: machineTx.id })
return t.oneOrNone(sql, [machineTx.id]).then(row => {
if (row && row.tx_version >= machineTx.txVersion)
throw new E.StaleTxError({ txId: machineTx.id })
return t.any(sql2, [machineTx.id])
.then(billRows => {
const dbTx = cashInLow.toObj(row)
return t.any(sql2, [machineTx.id]).then(billRows => {
const dbTx = cashInLow.toObj(row)
return preProcess(dbTx, machineTx, pi)
.then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx))
.then(r => {
return insertNewBills(t, billRows, machineTx)
.then(newBills => _.set('newBills', newBills, r))
})
return preProcess(dbTx, machineTx)
.then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx))
.then(r => {
return insertNewBills(t, billRows, machineTx).then(newBills =>
_.set('newBills', newBills, r),
)
})
})
})
}
return db.tx({ mode }, transaction)
}
function insertNewBills (t, billRows, machineTx) {
function insertNewBills(t, billRows, machineTx) {
const bills = pullNewBills(billRows, machineTx)
if (_.isEmpty(bills)) return Promise.resolve([])
const dbBills = _.map(cashInLow.massage, bills)
const billsByDestination = _.countBy(_.get(['destination_unit']) ,dbBills)
const billsByDestination = _.countBy(_.get(['destination_unit']), dbBills)
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time', 'destination_unit']
const columns = [
'id',
'fiat',
'fiat_code',
'crypto_code',
'cash_in_fee',
'cash_in_txs_id',
'device_time',
'destination_unit',
]
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
const deviceID = machineTx.deviceId
const sql2 = `update devices set recycler1 = recycler1 + $2, recycler2 = recycler2 + $3, recycler3 = recycler3 + $4, recycler4 = recycler4 + $5, recycler5 = recycler5 + $6, recycler6 = recycler6 + $7
where device_id = $1`
return t.none(sql2, [
deviceID,
_.defaultTo(0, billsByDestination.recycler1),
_.defaultTo(0, billsByDestination.recycler2),
_.defaultTo(0, billsByDestination.recycler3),
_.defaultTo(0, billsByDestination.recycler4),
_.defaultTo(0, billsByDestination.recycler5),
_.defaultTo(0, billsByDestination.recycler6)
])
return t
.none(sql2, [
deviceID,
_.defaultTo(0, billsByDestination.recycler1),
_.defaultTo(0, billsByDestination.recycler2),
_.defaultTo(0, billsByDestination.recycler3),
_.defaultTo(0, billsByDestination.recycler4),
_.defaultTo(0, billsByDestination.recycler5),
_.defaultTo(0, billsByDestination.recycler6),
])
.then(() => {
return t.none(sql)
})
.then(() => bills)
}
function pullNewBills (billRows, machineTx) {
function pullNewBills(billRows, machineTx) {
if (_.isEmpty(machineTx.bills)) return []
const toBill = _.mapKeys(_.camelCase)
@ -73,7 +83,7 @@ function pullNewBills (billRows, machineTx) {
return _.differenceBy(_.get('id'), machineTx.bills, bills)
}
function preProcess (dbTx, machineTx, pi) {
function preProcess(dbTx, machineTx) {
// Note: The way this works is if we're clear to send,
// we mark the transaction as sendPending.
//

View file

@ -8,26 +8,40 @@ const E = require('../error')
const PENDING_INTERVAL_MS = 60 * T.minutes
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
const massageFields = [
'direction',
'cryptoNetwork',
'bills',
'blacklisted',
'blacklistMessage',
'addressReuse',
'promoCodeApplied',
'validWalletScore',
'cashInFeeCrypto',
]
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
const massage = _.flow(_.omit(massageFields),
convertBigNumFields, _.mapKeys(_.snakeCase))
const massage = _.flow(
_.omit(massageFields),
convertBigNumFields,
_.mapKeys(_.snakeCase),
)
const massageUpdates = _.flow(_.omit(massageUpdateFields),
convertBigNumFields, _.mapKeys(_.snakeCase))
const massageUpdates = _.flow(
_.omit(massageUpdateFields),
convertBigNumFields,
_.mapKeys(_.snakeCase),
)
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
module.exports = { toObj, upsert, insert, update, massage, isClearToSend }
function convertBigNumFields (obj) {
function convertBigNumFields(obj) {
const convert = value =>
value && BN.isBigNumber(value)
? value.toString()
: value
value && BN.isBigNumber(value) ? value.toString() : value
return _.mapValues(convert, obj)
}
function toObj (row) {
function toObj(row) {
if (!row) return null
const keys = _.keys(row)
@ -35,7 +49,15 @@ function toObj (row) {
keys.forEach(key => {
const objKey = _.camelCase(key)
if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'commission_percentage', 'raw_ticker_price'])) {
if (
_.includes(key, [
'crypto_atoms',
'fiat',
'cash_in_fee',
'commission_percentage',
'raw_ticker_price',
])
) {
newObj[objKey] = new BN(row[key])
return
}
@ -48,35 +70,35 @@ function toObj (row) {
return newObj
}
function upsert (t, dbTx, preProcessedTx) {
function upsert(t, dbTx, preProcessedTx) {
if (!dbTx) {
return insert(t, preProcessedTx)
.then(tx => ({dbTx, tx}))
return insert(t, preProcessedTx).then(tx => ({ dbTx, tx }))
}
return update(t, dbTx, diff(dbTx, preProcessedTx))
.then(tx => ({dbTx, tx}))
return update(t, dbTx, diff(dbTx, preProcessedTx)).then(tx => ({ dbTx, tx }))
}
function insert (t, tx) {
function insert(t, tx) {
const dbTx = massage(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
return t.one(sql)
.then(toObj)
return t.one(sql).then(toObj)
}
function update (t, tx, changes) {
function update(t, tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = isFinalTxStage(changes) ? massage(changes) : massageUpdates(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
const dbChanges = isFinalTxStage(changes)
? massage(changes)
: massageUpdates(changes)
const sql =
pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [tx.id]) +
' returning *'
return t.one(sql)
.then(toObj)
return t.one(sql).then(toObj)
}
function diff (oldTx, newTx) {
function diff(oldTx, newTx) {
let updatedTx = {}
if (!oldTx) throw new Error('oldTx must not be null')
@ -89,10 +111,15 @@ function diff (oldTx, newTx) {
if (_.isEqualWith(nilEqual, oldField, newField)) return
if (!ensureRatchet(oldField, newField, fieldKey)) {
logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey)
logger.warn(
'Value from lamassu-machine would violate ratchet [%s]',
fieldKey,
)
logger.warn('Old tx: %j', oldTx)
logger.warn('New tx: %j', newTx)
throw new E.RatchetError('Value from lamassu-machine would violate ratchet')
throw new E.RatchetError(
'Value from lamassu-machine would violate ratchet',
)
}
updatedTx[fieldKey] = newField
@ -101,12 +128,29 @@ function diff (oldTx, newTx) {
return updatedTx
}
function ensureRatchet (oldField, newField, fieldKey) {
const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion', 'batched', 'discount']
const free = ['sendPending', 'error', 'errorCode', 'customerId', 'discountSource']
function ensureRatchet(oldField, newField, fieldKey) {
const monotonic = [
'cryptoAtoms',
'fiat',
'send',
'sendConfirmed',
'operatorCompleted',
'timedout',
'txVersion',
'batched',
'discount',
]
const free = [
'sendPending',
'error',
'errorCode',
'customerId',
'discountSource',
]
if (_.isNil(oldField)) return true
if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey)
if (_.includes(fieldKey, monotonic))
return isMonotonic(oldField, newField, fieldKey)
if (_.includes(fieldKey, free)) {
if (_.isNil(newField)) return false
@ -114,13 +158,14 @@ function ensureRatchet (oldField, newField, fieldKey) {
}
if (_.isNil(newField)) return false
if (BN.isBigNumber(oldField) && BN.isBigNumber(newField)) return new BN(oldField).eq(newField)
if (BN.isBigNumber(oldField) && BN.isBigNumber(newField))
return new BN(oldField).eq(newField)
if (oldField.toString() === newField.toString()) return true
return false
}
function isMonotonic (oldField, newField, fieldKey) {
function isMonotonic(oldField, newField, fieldKey) {
if (_.isNil(newField)) return false
if (_.isBoolean(oldField)) return oldField === newField || !oldField
if (BN.isBigNumber(oldField)) return oldField.lte(newField)
@ -129,20 +174,22 @@ function isMonotonic (oldField, newField, fieldKey) {
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
}
function nilEqual (a, b) {
function nilEqual(a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined
}
function isClearToSend (oldTx, newTx) {
function isClearToSend(oldTx, newTx) {
const now = Date.now()
return (newTx.send || newTx.batched) &&
return (
(newTx.send || newTx.batched) &&
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
(newTx.created > now - PENDING_INTERVAL_MS)
newTx.created > now - PENDING_INTERVAL_MS
)
}
function isFinalTxStage (txChanges) {
function isFinalTxStage(txChanges) {
return txChanges.send || txChanges.batched
}

View file

@ -25,93 +25,114 @@ case
else 'Pending'
end`
module.exports = { post, monitorPending, cancel, PENDING_INTERVAL, TRANSACTION_STATES }
module.exports = {
post,
monitorPending,
cancel,
PENDING_INTERVAL,
TRANSACTION_STATES,
}
function post (machineTx, pi) {
function post(machineTx, pi) {
logger.silly('Updating cashin tx:', machineTx)
return cashInAtomic.atomic(machineTx, pi)
.then(r => {
const updatedTx = r.tx
let addressReuse = false
return cashInAtomic.atomic(machineTx).then(r => {
const updatedTx = r.tx
let addressReuse = false
const promises = [settingsLoader.loadLatestConfig()]
const promises = [settingsLoader.loadLatestConfig()]
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
if (isFirstPost) {
promises.push(
checkForBlacklisted(updatedTx),
doesTxReuseAddress(updatedTx),
getWalletScore(updatedTx, pi)
)
}
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
if (isFirstPost) {
promises.push(
checkForBlacklisted(updatedTx),
doesTxReuseAddress(updatedTx),
getWalletScore(updatedTx, pi),
)
}
return Promise.all(promises)
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
const { rejectAddressReuse } = configManager.getCompliance(config)
const isBlacklisted = !!blacklisted
return Promise.all(promises).then(
([
config,
blacklisted = false,
isReusedAddress = false,
walletScore = null,
]) => {
const { rejectAddressReuse } = configManager.getCompliance(config)
const isBlacklisted = !!blacklisted
if (isBlacklisted) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
} else if (isReusedAddress && rejectAddressReuse) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
addressReuse = true
}
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(_.flow(
if (isBlacklisted) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
} else if (isReusedAddress && rejectAddressReuse) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
addressReuse = true
}
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
.then(changes =>
_.set(
'walletScore',
_.isNil(walletScore) ? null : walletScore.score,
changes,
),
)
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(
_.flow(
_.set('bills', machineTx.bills),
_.set('blacklisted', isBlacklisted),
_.set('blacklistMessage', blacklisted?.content),
_.set('addressReuse', addressReuse),
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
))
})
})
_.set(
'validWalletScore',
_.isNil(walletScore) || walletScore.isValid,
),
),
)
},
)
})
}
function registerTrades (pi, r) {
function registerTrades(pi, r) {
_.forEach(bill => pi.buy(bill, r.tx), r.newBills)
}
function logAction (rec, tx) {
function logAction(rec, tx) {
const action = {
tx_id: tx.id,
action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
error: rec.error,
error_code: rec.errorCode,
tx_hash: rec.txHash
tx_hash: rec.txHash,
}
const sql = pgp.helpers.insert(action, null, 'cash_in_actions')
return db.none(sql)
.then(_.constant(rec))
return db.none(sql).then(_.constant(rec))
}
function logActionById (action, _rec, txId) {
function logActionById(action, _rec, txId) {
const rec = _.assign(_rec, { action, tx_id: txId })
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
return db.none(sql)
}
function checkForBlacklisted (tx) {
function checkForBlacklisted(tx) {
return blacklist.blocked(tx.toAddress)
}
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
function postProcess(r, pi, isBlacklisted, addressReuse, walletScore) {
if (addressReuse) {
return Promise.resolve({
operatorCompleted: true,
error: 'Address Reused'
error: 'Address Reused',
})
}
if (isBlacklisted) {
return Promise.resolve({
operatorCompleted: true,
error: 'Blacklisted Address'
error: 'Blacklisted Address',
})
}
@ -120,7 +141,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
walletScore: walletScore.score,
operatorCompleted: true,
error: 'Chain analysis score is above defined threshold',
errorCode: 'scoreThresholdReached'
errorCode: 'scoreThresholdReached',
})
}
@ -128,7 +149,8 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({})
return pi.sendCoins(r.tx)
return pi
.sendCoins(r.tx)
.then(txObj => {
if (txObj.batched) {
return {
@ -136,7 +158,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
batchTime: 'now()^',
sendPending: true,
error: null,
errorCode: null
errorCode: null,
}
}
@ -147,7 +169,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
sendTime: 'now()^',
sendPending: false,
error: null,
errorCode: null
errorCode: null,
}
})
.catch(err => {
@ -161,17 +183,18 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
sendTime: 'now()^',
error: err.message,
errorCode: err.name,
sendPending: true
sendPending: true,
}
})
.then(sendRec => {
pi.notifyOperator(r.tx, sendRec)
.catch((err) => logger.error('Failure sending transaction notification', err))
pi.notifyOperator(r.tx, sendRec).catch(err =>
logger.error('Failure sending transaction notification', err),
)
return logAction(sendRec, r.tx)
})
}
function doesTxReuseAddress (tx) {
function doesTxReuseAddress(tx) {
const sql = `
SELECT EXISTS (
SELECT DISTINCT to_address FROM (
@ -181,15 +204,14 @@ function doesTxReuseAddress (tx) {
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
}
function getWalletScore (tx, pi) {
return pi.isWalletScoringEnabled(tx)
.then(isEnabled => {
if (!isEnabled) return null
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
})
function getWalletScore(tx, pi) {
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
if (!isEnabled) return null
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
})
}
function monitorPending (settings) {
function monitorPending(settings) {
const sql = `select * from cash_in_txs
where created > now() - interval $1
and send
@ -203,27 +225,29 @@ function monitorPending (settings) {
const tx = cashInLow.toObj(row)
const pi = plugins(settings, tx.deviceId)
return post(tx, pi)
.catch(logger.error)
return post(tx, pi).catch(logger.error)
}
return db.any(sql, [PENDING_INTERVAL, MAX_PENDING])
return db
.any(sql, [PENDING_INTERVAL, MAX_PENDING])
.then(rows => pEachSeries(rows, row => processPending(row)))
.catch(logger.error)
}
function cancel (txId) {
function cancel(txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
operator_completed: true,
batch_id: null
batch_id: null,
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [txId])
return (
pgp.helpers.update(updateRec, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [txId])
)
})
.then(sql => db.result(sql, false))
.then(res => {

View file

@ -1,51 +1,63 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
module.exports = {logDispense, logActionById, logAction, logError}
module.exports = { logDispense, logActionById, logAction, logError }
function logDispense (t, tx) {
const baseRec = {error: tx.error, error_code: tx.errorCode}
function logDispense(t, tx) {
const baseRec = { error: tx.error, error_code: tx.errorCode }
const rec = _.merge(mapDispense(tx), baseRec)
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
return logAction(t, action, rec, tx)
}
function logActionById (t, action, _rec, txId) {
const rec = _.assign(_rec, {action, tx_id: txId, redeem: false})
function logActionById(t, action, _rec, txId) {
const rec = _.assign(_rec, { action, tx_id: txId, redeem: false })
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return t.none(sql)
}
function logAction (t, action, _rec, tx) {
const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem, device_id: tx.deviceId})
function logAction(t, action, _rec, tx) {
const rec = _.assign(_rec, {
action,
tx_id: tx.id,
redeem: !!tx.redeem,
device_id: tx.deviceId,
})
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return t.none(sql)
.then(_.constant(tx))
return t.none(sql).then(_.constant(tx))
}
function logError (t, action, err, tx) {
return logAction(t, action, {
error: err.message,
error_code: err.name
}, tx)
function logError(t, action, err, tx) {
return logAction(
t,
action,
{
error: err.message,
error_code: err.name,
},
tx,
)
}
function mapDispense (tx) {
function mapDispense(tx) {
const bills = tx.bills
if (_.isEmpty(bills)) return {}
const res = {}
_.forEach(it => {
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
res[`provisioned_${suffix}`] = bills[it].provisioned
res[`denomination_${suffix}`] = bills[it].denomination
res[`dispensed_${suffix}`] = bills[it].dispensed
res[`rejected_${suffix}`] = bills[it].rejected
}, _.times(_.identity(), _.size(bills)))
_.forEach(
it => {
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
res[`provisioned_${suffix}`] = bills[it].provisioned
res[`denomination_${suffix}`] = bills[it].denomination
res[`dispensed_${suffix}`] = bills[it].dispensed
res[`rejected_${suffix}`] = bills[it].rejected
},
_.times(_.identity(), _.size(bills)),
)
return res
}

View file

@ -13,170 +13,208 @@ const toObj = helper.toObj
module.exports = { atomic }
function atomic (tx, pi, fromClient) {
function atomic(tx, pi, fromClient) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
function transaction (t) {
function transaction(t) {
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE'
return t.oneOrNone(sql, [tx.id])
return t
.oneOrNone(sql, [tx.id])
.then(toObj)
.then(oldTx => {
const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion)
const isStale = fromClient && oldTx && oldTx.txVersion >= tx.txVersion
if (isStale) throw new E.StaleTxError({ txId: tx.id })
// Server doesn't bump version, so we just prevent from version being older.
const isStaleFromServer = !fromClient && oldTx && (oldTx.txVersion > tx.txVersion)
if (isStaleFromServer) throw new Error('Stale Error: server triggered', tx.id)
const isStaleFromServer =
!fromClient && oldTx && oldTx.txVersion > tx.txVersion
if (isStaleFromServer)
throw new Error('Stale Error: server triggered', tx.id)
return preProcess(t, oldTx, tx, pi)
.then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx))
return preProcess(t, oldTx, tx, pi).then(preProcessedTx =>
cashOutLow.upsert(t, oldTx, preProcessedTx),
)
})
}
return db.tx({ mode }, transaction)
}
function preProcess (t, oldTx, newTx, pi) {
function preProcess(t, oldTx, newTx, pi) {
if (!oldTx) {
return pi.isHd(newTx)
return pi
.isHd(newTx)
.then(isHd => nextHd(t, isHd, newTx))
.then(newTxHd => {
return pi.newAddress(newTxHd)
.then(_.merge(newTxHd))
return pi.newAddress(newTxHd).then(_.merge(newTxHd))
})
.then(addressedTx => {
const rec = {
to_address: addressedTx.toAddress,
layer_2_address: addressedTx.layer2Address
layer_2_address: addressedTx.layer2Address,
}
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
})
.catch(err => {
pi.notifyOperator(newTx, { isRedemption: false, error: 'Error while provisioning address' })
.catch((err) => logger.error('Failure sending transaction notification', err))
return cashOutActions.logError(t, 'provisionAddress', err, newTx)
.then(() => { throw err })
pi.notifyOperator(newTx, {
isRedemption: false,
error: 'Error while provisioning address',
}).catch(err =>
logger.error('Failure sending transaction notification', err),
)
return cashOutActions
.logError(t, 'provisionAddress', err, newTx)
.then(() => {
throw err
})
})
}
return Promise.resolve(updateStatus(oldTx, newTx))
.then(updatedTx => {
if (updatedTx.status !== oldTx.status) {
const isZeroConf = pi.isZeroConf(updatedTx)
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
return Promise.resolve(updateStatus(oldTx, newTx)).then(updatedTx => {
if (updatedTx.status !== oldTx.status) {
const isZeroConf = pi.isZeroConf(updatedTx)
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
const rec = {
to_address: updatedTx.toAddress,
tx_hash: updatedTx.txHash
}
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
const rec = {
to_address: updatedTx.toAddress,
tx_hash: updatedTx.txHash,
}
const hasError = !oldTx.error && newTx.error
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
}
if (hasError || hasDispenseOccurred) {
return cashOutActions.logDispense(t, updatedTx)
.then(it => updateCassettes(t, updatedTx).then(() => it) )
.then((t) => {
pi.notifyOperator(updatedTx, { isRedemption: true })
.catch((err) => logger.error('Failure sending transaction notification', err))
return t
})
}
const hasError = !oldTx.error && newTx.error
const hasDispenseOccurred =
!oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
if (!oldTx.phone && newTx.phone) {
return cashOutActions.logAction(t, 'addPhone', {}, updatedTx)
}
if (hasError || hasDispenseOccurred) {
return cashOutActions
.logDispense(t, updatedTx)
.then(it => updateCassettes(t, updatedTx).then(() => it))
.then(t => {
pi.notifyOperator(updatedTx, { isRedemption: true }).catch(err =>
logger.error('Failure sending transaction notification', err),
)
return t
})
}
if (!oldTx.redeem && newTx.redeem) {
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
}
if (!oldTx.phone && newTx.phone) {
return cashOutActions.logAction(t, 'addPhone', {}, updatedTx)
}
return updatedTx
})
if (!oldTx.redeem && newTx.redeem) {
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
}
return updatedTx
})
}
function nextHd (t, isHd, tx) {
function nextHd(t, isHd, tx) {
if (!isHd) return Promise.resolve(tx)
return t.one("select nextval('hd_indices_seq') as hd_index")
return t
.one("select nextval('hd_indices_seq') as hd_index")
.then(row => _.set('hdIndex', row.hd_index, tx))
}
function updateCassettes (t, tx) {
function updateCassettes(t, tx) {
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
const billsStmt = _.join(', ')(_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(_.range(0, _.size(tx.bills))))
const billsStmt = _.join(', ')(
_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(
_.range(0, _.size(tx.bills)),
),
)
const returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills))
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
const values = []
_.forEach(it => values.push(
tx.bills[it].dispensed + tx.bills[it].rejected
), _.times(_.identity(), _.size(tx.bills)))
_.forEach(
it => values.push(tx.bills[it].dispensed + tx.bills[it].rejected),
_.times(_.identity(), _.size(tx.bills)),
)
values.push(tx.deviceId)
return t.one(sql, values)
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published', 'rejected']) &&
function wasJustAuthorized(oldTx, newTx, isZeroConf) {
const isAuthorized = () =>
_.includes(oldTx.status, ['notSeen', 'published', 'rejected']) &&
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized', 'rejected']) &&
_.includes(newTx.status, ['instant', 'confirmed'])
const isConfirmed = () =>
_.includes(oldTx.status, [
'notSeen',
'published',
'authorized',
'rejected',
]) && _.includes(newTx.status, ['instant', 'confirmed'])
return isZeroConf ? isAuthorized() : isConfirmed()
}
function isPublished (status) {
return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed'])
function isPublished(status) {
return _.includes(status, [
'published',
'rejected',
'authorized',
'instant',
'confirmed',
])
}
function isConfirmed (status) {
function isConfirmed(status) {
return status === 'confirmed'
}
function updateStatus (oldTx, newTx) {
function updateStatus(oldTx, newTx) {
const oldStatus = oldTx.status
const newStatus = ratchetStatus(oldStatus, newTx.status)
const publishedAt = !oldTx.publishedAt && isPublished(newStatus)
? 'now()^'
: undefined
const publishedAt =
!oldTx.publishedAt && isPublished(newStatus) ? 'now()^' : undefined
const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus)
? 'now()^'
: undefined
const confirmedAt =
!oldTx.confirmedAt && isConfirmed(newStatus) ? 'now()^' : undefined
const updateRec = {
publishedAt,
confirmedAt,
status: newStatus
status: newStatus,
}
return _.merge(newTx, updateRec)
}
function ratchetStatus (oldStatus, newStatus) {
const statusOrder = ['notSeen', 'published', 'rejected',
'authorized', 'instant', 'confirmed']
function ratchetStatus(oldStatus, newStatus) {
const statusOrder = [
'notSeen',
'published',
'rejected',
'authorized',
'instant',
'confirmed',
]
if (oldStatus === newStatus) return oldStatus
if (newStatus === 'insufficientFunds') return newStatus
const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus))
const idx = Math.max(
statusOrder.indexOf(oldStatus),
statusOrder.indexOf(newStatus),
)
return statusOrder[idx]
}
function dispenseOccurred (bills) {
function dispenseOccurred(bills) {
if (_.isEmpty(bills)) return false
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
}

View file

@ -40,18 +40,31 @@ const SNAKE_CASE_BILL_FIELDS = [
'provisioned_recycler_3',
'provisioned_recycler_4',
'provisioned_recycler_5',
'provisioned_recycler_6'
'provisioned_recycler_6',
]
const BILL_FIELDS = _.map(_.camelCase, SNAKE_CASE_BILL_FIELDS)
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
module.exports = {
redeemableTxs,
toObj,
toDb,
REDEEMABLE_AGE,
CASH_OUT_TRANSACTION_STATES,
}
const mapValuesWithKey = _.mapValues.convert({cap: false})
const mapValuesWithKey = _.mapValues.convert({ cap: false })
function convertBigNumFields (obj) {
function convertBigNumFields(obj) {
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()
@ -59,62 +72,62 @@ function convertBigNumFields (obj) {
}
// Only test isNil for these fields since the others should not be empty.
if (_.includes(key, [ 'commissionPercentage', 'rawTickerPrice' ]) && !_.isNil(value)) {
if (
_.includes(key, ['commissionPercentage', 'rawTickerPrice']) &&
!_.isNil(value)
) {
return value.toString()
}
return value
}
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
? key + '#'
: key
const convertKey = key =>
_.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
function convertField (key) {
function convertField(key) {
return _.snakeCase(key)
}
function addDbBills (tx) {
function addDbBills(tx) {
const bills = tx.bills
if (_.isEmpty(bills)) return tx
const billsObj = _.flow(
_.reduce(
(acc, value) => {
const suffix = _.snakeCase(value.name.replace(/cassette/gi, ''))
return {
...acc,
[`provisioned_${suffix}`]: value.provisioned,
[`denomination_${suffix}`]: value.denomination
}
},
{}
),
_.reduce((acc, value) => {
const suffix = _.snakeCase(value.name.replace(/cassette/gi, ''))
return {
...acc,
[`provisioned_${suffix}`]: value.provisioned,
[`denomination_${suffix}`]: value.denomination,
}
}, {}),
it => {
const missingKeys = _.reduce(
(acc, value) => {
return _.assign({ [value]: 0 })(acc)
},
{}
)(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it)))
const missingKeys = _.reduce((acc, value) => {
return _.assign({ [value]: 0 })(acc)
}, {})(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it)))
return _.assign(missingKeys, it)
}
},
)(bills)
return _.assign(tx, billsObj)
}
function toDb (tx) {
const massager = _.flow(convertBigNumFields, addDbBills,
_.omit(['direction', 'bills', 'promoCodeApplied']), _.mapKeys(convertField))
function toDb(tx) {
const massager = _.flow(
convertBigNumFields,
addDbBills,
_.omit(['direction', 'bills', 'promoCodeApplied']),
_.mapKeys(convertField),
)
return massager(tx)
}
function toObj (row) {
function toObj(row) {
if (!row) return null
const keys = _.keys(row)
@ -126,7 +139,14 @@ function toObj (row) {
newObj[objKey] = new BN(row[key])
return
}
if (_.includes(key, ['crypto_atoms', 'fiat', 'commission_percentage', 'raw_ticker_price'])) {
if (
_.includes(key, [
'crypto_atoms',
'fiat',
'commission_percentage',
'raw_ticker_price',
])
) {
newObj[objKey] = new BN(row[key])
return
}
@ -137,11 +157,20 @@ function toObj (row) {
newObj.direction = 'cashOut'
if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj
if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) throw new Error('Missing cassette values')
if (_.some(_.isNil, _.at(BILL_FIELDS, newObj)))
throw new Error('Missing cassette values')
const billFieldsArr = _.concat(
_.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)),
_.map(it => ({ name: `recycler${it + 1}`, denomination: newObj[`denominationRecycler${it + 1}`], provisioned: newObj[`provisionedRecycler${it + 1}`] }))(_.range(0, MAX_RECYCLERS)),
_.map(it => ({
name: `cassette${it + 1}`,
denomination: newObj[`denomination${it + 1}`],
provisioned: newObj[`provisioned${it + 1}`],
}))(_.range(0, MAX_CASSETTES)),
_.map(it => ({
name: `recycler${it + 1}`,
denomination: newObj[`denominationRecycler${it + 1}`],
provisioned: newObj[`provisionedRecycler${it + 1}`],
}))(_.range(0, MAX_RECYCLERS)),
)
// There can't be bills with denomination === 0.
@ -151,7 +180,7 @@ function toObj (row) {
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
}
function redeemableTxs (deviceId) {
function redeemableTxs(deviceId) {
const sql = `select * from cash_out_txs
where device_id=$1
and redeem=$2
@ -164,6 +193,5 @@ function redeemableTxs (deviceId) {
)
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
.then(_.map(toObj))
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE]).then(_.map(toObj))
}

View file

@ -7,73 +7,91 @@ const { anonymousCustomer } = require('../constants')
const toDb = helper.toDb
const toObj = helper.toObj
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
'receivedCryptoAtoms', 'walletScore', 'customerId' ]
const UPDATEABLE_FIELDS = [
'txHash',
'txVersion',
'status',
'dispense',
'dispenseConfirmed',
'notified',
'redeem',
'phone',
'error',
'swept',
'publishedAt',
'confirmedAt',
'errorCode',
'receivedCryptoAtoms',
'walletScore',
'customerId',
]
module.exports = {upsert, update, insert}
module.exports = { upsert, update, insert }
function upsert (t, oldTx, tx) {
function upsert(t, oldTx, tx) {
if (!oldTx) {
return insert(t, tx)
.then(newTx => [oldTx, newTx])
return insert(t, tx).then(newTx => [oldTx, newTx])
}
return update(t, tx, diff(oldTx, tx))
.then(newTx => [oldTx, newTx, tx.justAuthorized])
return update(t, tx, diff(oldTx, tx)).then(newTx => [
oldTx,
newTx,
tx.justAuthorized,
])
}
function insert (t, tx) {
function insert(t, tx) {
const dbTx = toDb(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *'
return t.one(sql)
.then(toObj)
return t.one(sql).then(toObj)
}
function update (t, tx, changes) {
function update(t, tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = toDb(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
const sql =
pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [tx.id])
const newTx = _.merge(tx, changes)
return t.none(sql)
.then(() => newTx)
return t.none(sql).then(() => newTx)
}
function diff (oldTx, newTx) {
function diff(oldTx, newTx) {
let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => {
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey]))
return
// We never null out an existing field
if (oldTx && _.isNil(newTx[fieldKey])) return updatedTx[fieldKey] = oldTx[fieldKey]
if (oldTx && _.isNil(newTx[fieldKey]))
return (updatedTx[fieldKey] = oldTx[fieldKey])
switch (fieldKey) {
case 'customerId':
if (oldTx.customerId === anonymousCustomer.uuid) {
return updatedTx['customerId'] = newTx['customerId']
return (updatedTx['customerId'] = newTx['customerId'])
}
return
// prevent dispense changing from 'true' to 'false'
case 'dispense':
if (!oldTx.dispense) {
return updatedTx[fieldKey] = newTx[fieldKey]
return (updatedTx[fieldKey] = newTx[fieldKey])
}
return
default:
return updatedTx[fieldKey] = newTx[fieldKey]
return (updatedTx[fieldKey] = newTx[fieldKey])
}
})
return updatedTx
}
function nilEqual (a, b) {
function nilEqual(a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined

View file

@ -20,7 +20,7 @@ module.exports = {
monitorLiveIncoming,
monitorStaleIncoming,
monitorUnnotified,
cancel
cancel,
}
const STALE_INCOMING_TX_AGE = T.day
@ -31,38 +31,40 @@ const INSUFFICIENT_FUNDS_CODE = 570
const toObj = helper.toObj
function selfPost (tx, pi) {
function selfPost(tx, pi) {
return post(tx, pi, false)
}
function post (tx, pi, fromClient = true) {
function post(tx, pi, fromClient = true) {
logger.silly('Updating cashout -- tx:', JSON.stringify(tx))
logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient))
return cashOutAtomic.atomic(tx, pi, fromClient)
.then(txVector => {
const [, newTx, justAuthorized] = txVector
return postProcess(txVector, justAuthorized, pi)
.then(changes => cashOutLow.update(db, newTx, changes))
})
return cashOutAtomic.atomic(tx, pi, fromClient).then(txVector => {
const [, newTx, justAuthorized] = txVector
return postProcess(txVector, justAuthorized, pi).then(changes =>
cashOutLow.update(db, newTx, changes),
)
})
}
function postProcess (txVector, justAuthorized, pi) {
function postProcess(txVector, justAuthorized, pi) {
const [oldTx, newTx] = txVector
if (justAuthorized) {
pi.sell(newTx)
pi.notifyOperator(newTx, { isRedemption: false })
.catch((err) => logger.error('Failure sending transaction notification', err))
pi.notifyOperator(newTx, { isRedemption: false }).catch(err =>
logger.error('Failure sending transaction notification', err),
)
}
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
return pi.buildAvailableUnits(newTx.id)
return pi
.buildAvailableUnits(newTx.id)
.then(units => {
units = _.concat(units.cassettes, units.recyclers)
logger.silly('Computing bills to dispense:', {
txId: newTx.id,
units: units,
fiat: newTx.fiat
fiat: newTx.fiat,
})
const bills = billMath.makeChange(units, newTx.fiat)
logger.silly('Bills to dispense:', JSON.stringify(bills))
@ -73,27 +75,38 @@ function postProcess (txVector, justAuthorized, pi) {
.then(bills => {
const rec = {}
_.forEach(it => {
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
rec[`provisioned_${suffix}`] = bills[it].provisioned
rec[`denomination_${suffix}`] = bills[it].denomination
}, _.times(_.identity(), _.size(bills)))
_.forEach(
it => {
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
rec[`provisioned_${suffix}`] = bills[it].provisioned
rec[`denomination_${suffix}`] = bills[it].denomination
},
_.times(_.identity(), _.size(bills)),
)
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
return cashOutActions
.logAction(db, 'provisionNotes', rec, newTx)
.then(_.constant({ bills }))
})
.catch(err => {
pi.notifyOperator(newTx, { error: err.message, isRedemption: true })
.catch((err) => logger.error('Failure sending transaction notification', err))
return cashOutActions.logError(db, 'provisionNotesError', err, newTx)
.then(() => { throw err })
pi.notifyOperator(newTx, {
error: err.message,
isRedemption: true,
}).catch(err =>
logger.error('Failure sending transaction notification', err),
)
return cashOutActions
.logError(db, 'provisionNotesError', err, newTx)
.then(() => {
throw err
})
})
}
return Promise.resolve({})
}
function fetchOpenTxs (statuses, fromAge, toAge) {
function fetchOpenTxs(statuses, fromAge, toAge) {
const sql = `select *
from cash_out_txs
where ((extract(epoch from (now() - created))) * 1000)>$1
@ -103,20 +116,27 @@ function fetchOpenTxs (statuses, fromAge, toAge) {
const statusClause = _.map(pgp.as.text, statuses).join(',')
return db.any(sql, [fromAge, toAge, statusClause])
return db
.any(sql, [fromAge, toAge, statusClause])
.then(rows => rows.map(toObj))
}
function processTxStatus (tx, settings) {
function processTxStatus(tx, settings) {
const pi = plugins(settings, tx.deviceId)
return pi.getStatus(tx)
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
return pi
.getStatus(tx)
.then(res =>
_.assign(tx, {
receivedCryptoAtoms: res.receivedCryptoAtoms,
status: res.status,
}),
)
.then(_tx => getWalletScore(_tx, pi))
.then(_tx => selfPost(_tx, pi))
}
function getWalletScore (tx, pi) {
function getWalletScore(tx, pi) {
const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds']
if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) {
@ -124,40 +144,54 @@ function getWalletScore (tx, pi) {
}
// Transaction shows up on the blockchain, we can request the sender address
return pi.isWalletScoringEnabled(tx)
.then(isEnabled => {
if (!isEnabled) return tx
return pi.rateTransaction(tx)
.then(res =>
res.isValid
? _.assign(tx, { walletScore: res.score })
: _.assign(tx, {
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
if (!isEnabled) return tx
return pi
.rateTransaction(tx)
.then(res =>
res.isValid
? _.assign(tx, { walletScore: res.score })
: _.assign(tx, {
walletScore: res.score,
error: 'Chain analysis score is above defined threshold',
errorCode: 'scoreThresholdReached',
dispense: true
})
)
.catch(error => _.assign(tx, {
dispense: true,
}),
)
.catch(error =>
_.assign(tx, {
walletScore: 10,
error: `Failure getting address score: ${error.message}`,
errorCode: 'walletScoringError',
dispense: true
}))
})
dispense: true,
}),
)
})
}
function monitorLiveIncoming (settings) {
function monitorLiveIncoming(settings) {
const statuses = ['notSeen', 'published', 'insufficientFunds']
return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
}
function monitorStaleIncoming (settings) {
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
function monitorStaleIncoming(settings) {
const statuses = [
'notSeen',
'published',
'authorized',
'instant',
'rejected',
'insufficientFunds',
]
return monitorIncoming(
settings,
statuses,
STALE_LIVE_INCOMING_TX_AGE,
STALE_INCOMING_TX_AGE,
)
}
function monitorIncoming (settings, statuses, fromAge, toAge) {
function monitorIncoming(settings, statuses, fromAge, toAge) {
return fetchOpenTxs(statuses, fromAge, toAge)
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
.catch(err => {
@ -169,7 +203,7 @@ function monitorIncoming (settings, statuses, fromAge, toAge) {
})
}
function monitorUnnotified (settings) {
function monitorUnnotified(settings) {
const sql = `select *
from cash_out_txs
where ((extract(epoch from (now() - created))) * 1000)<$1
@ -179,23 +213,26 @@ function monitorUnnotified (settings) {
and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)`
const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx)
return db.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE])
return db
.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE])
.then(rows => _.map(toObj, rows))
.then(txs => Promise.all(txs.map(notify)))
.catch(logger.error)
}
function cancel (txId) {
function cancel(txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
dispense: true
dispense: true,
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [txId])
return (
pgp.helpers.update(updateRec, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [txId])
)
})
.then(sql => db.result(sql, false))
.then(res => {

View file

@ -4,8 +4,9 @@ const _ = require('lodash/fp')
const uuid = require('uuid')
const camelize = require('./utils')
function createCashboxBatch (deviceId, cashboxCount) {
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
function createCashboxBatch(deviceId, cashboxCount) {
if (_.isEqual(0, cashboxCount))
throw new Error('Cash box is empty. Cash box batch could not be created.')
const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
const sql2 = `
UPDATE bills SET cashbox_batch_id=$1
@ -24,55 +25,93 @@ function createCashboxBatch (deviceId, cashboxCount) {
const q1 = t.one(sql, [batchId, deviceId])
const q2 = t.none(sql2, [batchId, deviceId])
const q3 = t.none(sql3, [batchId, deviceId])
return t.batch([q1, q2, q3])
.then(([it]) => it)
return t.batch([q1, q2, q3]).then(([it]) => it)
})
}
function updateMachineWithBatch (machineContext, oldCashboxCount) {
function updateMachineWithBatch(machineContext, oldCashboxCount) {
const cashUnits = machineContext.cashUnits
const cashUnitNames = ['cashbox', 'cassette1', 'cassette2', 'cassette3', 'cassette4', 'recycler1', 'recycler2', 'recycler3', 'recycler4', 'recycler5', 'recycler6']
const isValidContext = _.has(['deviceId', 'cashUnits'], machineContext) && _.has(cashUnitNames, cashUnits)
const cassettes = _.filter(it => !_.isNil(it))([cashUnits.cassette1, cashUnits.cassette2, cashUnits.cassette3, cashUnits.cassette4])
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(cassettes))
const cashUnitNames = [
'cashbox',
'cassette1',
'cassette2',
'cassette3',
'cassette4',
'recycler1',
'recycler2',
'recycler3',
'recycler4',
'recycler5',
'recycler6',
]
const isValidContext =
_.has(['deviceId', 'cashUnits'], machineContext) &&
_.has(cashUnitNames, cashUnits)
const cassettes = _.filter(it => !_.isNil(it))([
cashUnits.cassette1,
cashUnits.cassette2,
cashUnits.cassette3,
cashUnits.cassette4,
])
const isCassetteAmountWithinRange = _.inRange(
constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1,
_.size(cassettes),
)
if (!isValidContext && !isCassetteAmountWithinRange)
throw new Error('Insufficient info to create a new cashbox batch')
if (_.isEqual(0, oldCashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
if (_.isEqual(0, oldCashboxCount))
throw new Error('Cash box is empty. Cash box batch could not be created.')
return db.tx(t => {
const deviceId = machineContext.deviceId
const batchId = uuid.v4()
const q1 = t.none(`INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, [batchId, deviceId])
const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
const q1 = t.none(
`INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`,
[batchId, deviceId],
)
const q2 = t.none(
`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
cash_in_txs.device_id = $2 AND
bills.destination_unit = 'cashbox' AND
bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
const q3 = t.none(`UPDATE empty_unit_bills SET cashbox_batch_id=$1
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
const q4 = t.none(`
bills.cashbox_batch_id IS NULL`,
[batchId, deviceId],
)
const q3 = t.none(
`UPDATE empty_unit_bills SET cashbox_batch_id=$1
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`,
[batchId, deviceId],
)
const q4 = t.none(
`
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6) WHERE device_id=$11
`, [
cashUnits.cassette1,
cashUnits.cassette2,
cashUnits.cassette3,
cashUnits.cassette4,
cashUnits.recycler1,
cashUnits.recycler2,
cashUnits.recycler3,
cashUnits.recycler4,
cashUnits.recycler5,
cashUnits.recycler6,
machineContext.deviceId
])
`,
[
cashUnits.cassette1,
cashUnits.cassette2,
cashUnits.cassette3,
cashUnits.cassette4,
cashUnits.recycler1,
cashUnits.recycler2,
cashUnits.recycler3,
cashUnits.recycler4,
cashUnits.recycler5,
cashUnits.recycler6,
machineContext.deviceId,
],
)
return t.batch([q1, q2, q3, q4])
})
}
function getBatches (from = new Date(0).toISOString(), until = new Date().toISOString()) {
function getBatches(
from = new Date(0).toISOString(),
until = new Date().toISOString(),
) {
const sql = `
SELECT
cuo.id,
@ -96,28 +135,25 @@ function getBatches (from = new Date(0).toISOString(), until = new Date().toISOS
return db.any(sql, [from, until]).then(camelize)
}
function editBatchById (id, performedBy) {
function editBatchById(id, performedBy) {
const sql = `UPDATE cash_unit_operation SET performed_by=$1 WHERE id=$2 AND cuo.operation_type = 'cash-box-empty'`
return db.none(sql, [performedBy, id])
}
function logFormatter (data) {
return _.map(
it => {
return {
id: it.id,
deviceId: it.deviceId,
created: it.created,
operationType: it.operationType,
billCount: it.billCount,
fiatTotal: it.fiatTotal
}
},
data
)
function logFormatter(data) {
return _.map(it => {
return {
id: it.id,
deviceId: it.deviceId,
created: it.created,
operationType: it.operationType,
billCount: it.billCount,
fiatTotal: it.fiatTotal,
}
}, data)
}
function getMachineUnbatchedBills (deviceId) {
function getMachineUnbatchedBills(deviceId) {
const sql = `
SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs
ON b.cash_in_txs_id = cash_in_txs.id
@ -125,12 +161,13 @@ function getMachineUnbatchedBills (deviceId) {
GROUP BY cash_in_txs.device_id
`
return db.oneOrNone(sql, [deviceId])
return db
.oneOrNone(sql, [deviceId])
.then(res => _.mapKeys(it => _.camelCase(it), res))
.then(logFormatterSingle)
}
function getBatchById (id) {
function getBatchById(id) {
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
FROM cash_unit_operation AS cb
@ -139,14 +176,22 @@ function getBatchById (id) {
GROUP BY cb.id
`
return db.oneOrNone(sql, [id]).then(res => _.mapKeys(it => _.camelCase(it), res))
return db
.oneOrNone(sql, [id])
.then(res => _.mapKeys(it => _.camelCase(it), res))
.then(logFormatterSingle)
}
function logFormatterSingle (data) {
function logFormatterSingle(data) {
const bills = _.filter(
it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)),
data.bills
it =>
!(
_.isNil(it) ||
_.isNil(it.fiat_code) ||
_.isNil(it.fiat) ||
_.isNaN(it.fiat)
),
data.bills,
)
return {
@ -161,9 +206,9 @@ function logFormatterSingle (data) {
return acc
},
{},
bills
bills,
),
billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills)
billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills),
}
}
@ -174,5 +219,5 @@ module.exports = {
editBatchById,
getBatchById,
getMachineUnbatchedBills,
logFormatter
logFormatter,
}

View file

@ -9,16 +9,13 @@
*/
const prepare_denominations = denominations =>
JSON.parse(JSON.stringify(denominations))
.sort(([d1, c1], [d2, c2]) => d1 < d2)
.sort(([d1], [d2]) => d1 < d2)
.reduce(
([csum, denoms], [denom, count]) => {
csum += denom*count
return [
csum,
[{ denom, count, csum }].concat(denoms)
]
csum += denom * count
return [csum, [{ denom, count, csum }].concat(denoms)]
},
[0, []]
[0, []],
)[1] /* ([csum, denoms]) => denoms */
const max_denomination_multiplicity = (denom, count, target) =>
@ -38,18 +35,18 @@ const memo_get = (memo, target, denom) => {
const memo_set = (memo, target, denom, solution) => {
let denom_solutions = memo[target]
if (denom_solutions === undefined)
memo[target] = denom_solutions = {}
return denom_solutions[denom] = solution
if (denom_solutions === undefined) memo[target] = denom_solutions = {}
return (denom_solutions[denom] = solution)
}
const check = (solution, target) =>
!solution
|| target === solution.reduce((sum, [denom, provisioned]) => sum + denom*provisioned, 0)
!solution ||
target ===
solution.reduce((sum, [denom, provisioned]) => sum + denom * provisioned, 0)
const model = denominations => ({
denominations: prepare_denominations(denominations),
memo: {}
memo: {},
})
/*
@ -73,16 +70,19 @@ const solve = (model, target) => {
* of the denominations, or if the target is not divisible by any of the
* denominations
*/
if (target > csum)
return memo_set(memo, target, denom, false)
if (target > csum) return memo_set(memo, target, denom, false)
let solution = memo_get(memo, target, denom)
if (solution === false) continue /* not here, keep looking */
if (solution) return solution /* we've previously computed a solution */
/* solution === null */
for (let nd = max_denomination_multiplicity(denom, count, target); nd >= 0; nd--) {
solution = coin_change(didx+1, target - denom*nd)
for (
let nd = max_denomination_multiplicity(denom, count, target);
nd >= 0;
nd--
) {
solution = coin_change(didx + 1, target - denom * nd)
if (solution)
return memo_set(memo, target, denom, [[denom, nd]].concat(solution))
}

View file

@ -18,9 +18,11 @@ const STALE_INTERVAL = '2 minutes'
module.exports = { update }
function mapCoin (rates, deviceId, settings, cryptoCode) {
function mapCoin(rates, deviceId, settings, cryptoCode) {
const config = settings.config
const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null }
const buildedRates = plugins(settings, deviceId).buildRates(rates)[
cryptoCode
] || { cashIn: null, cashOut: null }
const commissions = configManager.getCommissions(cryptoCode, deviceId, config)
const coinAtmRadar = configManager.getCoinAtmRadar(config)
@ -30,8 +32,12 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
const cashInRate = showCommissions
? _.invoke('cashIn.toNumber', buildedRates)
: null
const cashOutRate = showCommissions
? _.invoke('cashOut.toNumber', buildedRates)
: null
return {
cryptoCode,
@ -40,11 +46,11 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
cashInFixedFee,
cashOutFixedFee,
cashInRate,
cashOutRate
cashOutRate,
}
}
function mapIdentification (config) {
function mapIdentification(config) {
const triggers = configManager.getTriggers(config)
return {
@ -52,11 +58,11 @@ function mapIdentification (config) {
isPalmVein: false,
isPhoto: complianceTriggers.hasFacephoto(triggers),
isIdDocScan: complianceTriggers.hasIdScan(triggers),
isFingerprint: false
isFingerprint: false,
}
}
function mapMachine (rates, settings, machineRow) {
function mapMachine(rates, settings, machineRow) {
const deviceId = machineRow.device_id
const config = settings.config
@ -69,10 +75,15 @@ function mapMachine (rates, settings, machineRow) {
const lastOnline = machineRow.last_online.toISOString()
const status = machineRow.stale ? 'online' : 'offline'
const showLimitsAndVerification = coinAtmRadar.limitsAndVerification
const cashLimit = showLimitsAndVerification ? (_.get('threshold', complianceTriggers.getCashLimit(triggers)) || Infinity) : null
const cashLimit = showLimitsAndVerification
? _.get('threshold', complianceTriggers.getCashLimit(triggers)) || Infinity
: null
const cryptoCurrencies = locale.cryptoCurrencies
const identification = mapIdentification(config)
const coins = _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
const coins = _.map(
_.partial(mapCoin, [rates, deviceId, settings]),
cryptoCurrencies,
)
return {
machineId: deviceId,
address: {
@ -80,12 +91,12 @@ function mapMachine (rates, settings, machineRow) {
city: null,
region: null,
postalCode: null,
country: null
country: null,
},
location: {
name: null,
url: null,
phone: null
phone: null,
},
status,
lastOnline,
@ -98,20 +109,21 @@ function mapMachine (rates, settings, machineRow) {
cashOutDailyLimit: cashLimit,
fiatCurrency: locale.fiatCurrency,
identification,
coins
coins,
}
}
function getMachines (rates, settings) {
function getMachines(rates, settings) {
const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices
where display=TRUE and
paired=TRUE
order by created`
return db.any(sql, [STALE_INTERVAL])
return db
.any(sql, [STALE_INTERVAL])
.then(_.map(_.partial(mapMachine, [rates, settings])))
}
function sendRadar (data) {
function sendRadar(data) {
const url = COIN_ATM_RADAR_URL
if (_.isEmpty(url)) {
@ -123,31 +135,32 @@ function sendRadar (data) {
method: 'post',
data,
timeout: TIMEOUT,
maxContentLength: MAX_CONTENT_LENGTH
maxContentLength: MAX_CONTENT_LENGTH,
}
return axios.default(config)
.then(r => logger.info(r.status))
return axios.default(config).then(r => logger.info(r.status))
}
function mapRecord (rates, settings) {
function mapRecord(rates, settings) {
const timestamp = new Date().toISOString()
return Promise.all([getMachines(rates, settings), getOperatorId('coinatmradar')])
.then(([machines, operatorId]) => {
return {
operatorId: operatorId,
operator: {
name: null,
phone: null,
email: null
},
timestamp,
machines
}
})
return Promise.all([
getMachines(rates, settings),
getOperatorId('coinatmradar'),
]).then(([machines, operatorId]) => {
return {
operatorId: operatorId,
operator: {
name: null,
phone: null,
email: null,
},
timestamp,
machines,
}
})
}
function update (rates, settings) {
function update(rates, settings) {
const coinAtmRadar = configManager.getCoinAtmRadar(settings.config)
if (!coinAtmRadar.active) return Promise.resolve()

View file

@ -80,7 +80,7 @@ const settings = {
threshold: 123,
id: '9c3b5af8-b1d1-4125-b169-0e913b33894c',
direction: 'both',
triggerType: 'txAmount'
triggerType: 'txAmount',
},
{
requirement: 'sms',
@ -88,7 +88,7 @@ const settings = {
thresholdDays: 1,
id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b',
direction: 'both',
triggerType: 'txVelocity'
triggerType: 'txVelocity',
},
{
requirement: 'sms',
@ -96,58 +96,58 @@ const settings = {
thresholdDays: 1,
id: '6ac38fe6-172c-48a4-8a7f-605213cbd600',
direction: 'both',
triggerType: 'txVolume'
}
triggerType: 'txVolume',
},
],
notifications_sms_transactions: true,
notifications_highValueTransaction: 50
notifications_highValueTransaction: 50,
},
accounts: {}
accounts: {},
}
const rates = [
{
rates: {
ask: new BN(19164.3),
bid: new BN(19164.2)
bid: new BN(19164.2),
},
timestamp: +new Date()
timestamp: +new Date(),
},
{
rates: {
ask: new BN(594.54),
bid: new BN(594.09)
bid: new BN(594.09),
},
timestamp: +new Date()
timestamp: +new Date(),
},
{
rates: {
ask: new BN(84.38),
bid: new BN(84.37)
bid: new BN(84.37),
},
timestamp: +new Date()
timestamp: +new Date(),
},
{
rates: {
ask: new BN(102.8),
bid: new BN(101.64)
bid: new BN(101.64),
},
timestamp: +new Date()
timestamp: +new Date(),
},
{
rates: {
ask: new BN(74.91),
bid: new BN(74.12)
bid: new BN(74.12),
},
timestamp: +new Date()
timestamp: +new Date(),
},
{
rates: {
ask: new BN(284.4),
bid: new BN(284.4)
bid: new BN(284.4),
},
timestamp: +new Date()
}
timestamp: +new Date(),
},
]
const dbResponse = [
@ -155,32 +155,32 @@ const dbResponse = [
device_id:
'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
last_online: new Date('2020-11-16T13:11:03.169Z'),
stale: false
stale: false,
},
{
device_id:
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
last_online: new Date('2020-11-16T16:21:35.948Z'),
stale: false
stale: false,
},
{
device_id:
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
last_online: new Date('2020-11-19T15:07:57.089Z'),
stale: false
stale: false,
},
{
device_id:
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
last_online: new Date('2020-11-26T20:05:57.792Z'),
stale: false
stale: false,
},
{
device_id:
'490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88',
last_online: new Date('2020-12-04T16:48:05.129Z'),
stale: false
}
stale: false,
},
]
function validateData(data) {
@ -189,7 +189,7 @@ function validateData(data) {
operator: yup.object().shape({
name: yup.string().nullable(),
phone: yup.string().nullable(),
email: yup.string().email().nullable()
email: yup.string().email().nullable(),
}),
timestamp: yup.string().required('timestamp not provided'),
machines: yup.array().of(
@ -200,12 +200,12 @@ function validateData(data) {
city: yup.string().nullable(),
region: yup.string().nullable(),
postalCode: yup.string().nullable(),
country: yup.string().nullable()
country: yup.string().nullable(),
}),
location: yup.object().required('location object not provided').shape({
name: yup.string().nullable(),
url: yup.string().nullable(),
phone: yup.string().nullable()
phone: yup.string().nullable(),
}),
status: yup
.string()
@ -231,7 +231,7 @@ function validateData(data) {
.required('isIdDocScan boolean not defined'),
isFingerprint: yup
.boolean()
.required('isFingerprint boolean not defined')
.required('isFingerprint boolean not defined'),
}),
coins: yup.array().of(
yup.object().shape({
@ -240,11 +240,11 @@ function validateData(data) {
cashOutFee: yup.number().nullable(),
cashInFixedFee: yup.number().nullable(),
cashInRate: yup.number().nullable(),
cashOutRate: yup.number().nullable()
})
)
})
)
cashOutRate: yup.number().nullable(),
}),
),
}),
),
})
return schema.validate(data)
}
@ -252,13 +252,13 @@ function validateData(data) {
test('Verify axios request schema', async () => {
const axios = require('axios')
jest.spyOn(axios, 'default').mockImplementation(
jest.fn(req =>
validateData(req.data)
.then(() => ({ status: 'mock status 200' }))
.catch(e => fail(e))
jest
.spyOn(axios, 'default')
.mockImplementation(
jest.fn(req =>
validateData(req.data).then(() => ({ status: 'mock status 200' })),
),
)
)
db.any.mockResolvedValue(dbResponse)
await car.update(rates, settings)

View file

@ -2,31 +2,41 @@ const BN = require('./bn')
const configManager = require('./new-config-manager')
const { utils: coinUtils } = require('@lamassu/coins')
function truncateCrypto (cryptoAtoms, cryptoCode) {
function truncateCrypto(cryptoAtoms, cryptoCode) {
const DECIMAL_PLACES = 6
if (cryptoAtoms.eq(0)) return cryptoAtoms
const scale = coinUtils.getCryptoCurrency(cryptoCode).unitScale
const scaleFactor = BN(10).pow(scale)
return new BN(cryptoAtoms).integerValue(BN.ROUND_DOWN).div(scaleFactor)
.decimalPlaces(DECIMAL_PLACES).times(scaleFactor)
return new BN(cryptoAtoms)
.integerValue(BN.ROUND_DOWN)
.div(scaleFactor)
.decimalPlaces(DECIMAL_PLACES)
.times(scaleFactor)
}
function fiatToCrypto (tx, rec, deviceId, config) {
function fiatToCrypto(tx, rec, deviceId, config) {
const usableFiat = rec.fiat - rec.cashInFee
const commissions = configManager.getCommissions(tx.cryptoCode, deviceId, config)
const commissions = configManager.getCommissions(
tx.cryptoCode,
deviceId,
config,
)
const tickerRate = new BN(tx.rawTickerPrice)
const discount = getDiscountRate(tx.discount, commissions[tx.direction])
const rate = tickerRate.times(discount).decimalPlaces(5)
const unitScale = coinUtils.getCryptoCurrency(tx.cryptoCode).unitScale
const unitScaleFactor = new BN(10).pow(unitScale)
return truncateCrypto(new BN(usableFiat).div(rate.div(unitScaleFactor)), tx.cryptoCode)
return truncateCrypto(
new BN(usableFiat).div(rate.div(unitScaleFactor)),
tx.cryptoCode,
)
}
function getDiscountRate (discount, commission) {
function getDiscountRate(discount, commission) {
const bnDiscount = discount ? new BN(discount) : new BN(0)
const bnCommission = new BN(commission)
const percentageDiscount = new BN(1).minus(bnDiscount.div(100))
@ -36,5 +46,5 @@ function getDiscountRate (discount, commission) {
module.exports = {
fiatToCrypto,
getDiscountRate
getDiscountRate,
}

View file

@ -8,20 +8,25 @@ const getPlugin = (settings, pluginCode) => {
const account = settings.accounts[pluginCode]
const plugin = ph.load(ph.COMPLIANCE, pluginCode)
return ({ plugin, account })
return { plugin, account }
}
const getStatus = (settings, service, customerId) => {
try {
const { plugin, account } = getPlugin(settings, service)
return plugin.getApplicantStatus(account, customerId)
.then((status) => ({
return plugin
.getApplicantStatus(account, customerId)
.then(status => ({
service,
status
status,
}))
.catch((error) => {
if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message)
.catch(error => {
if (error.response.status !== 404)
logger.error(
`Error getting applicant for service ${service}:`,
error.message,
)
return {
service: service,
status: null,
@ -34,28 +39,22 @@ const getStatus = (settings, service, customerId) => {
status: null,
})
}
}
const getStatusMap = (settings, customerExternalCompliance) => {
const triggers = configManager.getTriggers(settings.config)
const services = _.flow(
_.map('externalService'),
_.compact,
_.uniq
)(triggers)
const services = _.flow(_.map('externalService'), _.compact, _.uniq)(triggers)
const applicantPromises = _.map(service => {
return getStatus(settings, service, customerExternalCompliance)
})(services)
return Promise.all(applicantPromises)
.then((applicantResults) => {
return _.reduce((map, result) => {
if (result.status) map[result.service] = result.status
return map
}, {})(applicantResults)
})
return Promise.all(applicantPromises).then(applicantResults => {
return _.reduce((map, result) => {
if (result.status) map[result.service] = result.status
return map
}, {})(applicantResults)
})
}
const createApplicant = (settings, externalService, customerId) => {
@ -76,5 +75,5 @@ module.exports = {
getStatusMap,
getStatus,
createApplicant,
createLink
createLink,
}

View file

@ -1,26 +1,35 @@
const _ = require('lodash/fp')
function getBackwardsCompatibleTriggers (triggers) {
const filtered = _.filter(_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }))(triggers)
function getBackwardsCompatibleTriggers(triggers) {
const filtered = _.filter(
_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }),
)(triggers)
const grouped = _.groupBy(_.prop('requirement'))(filtered)
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped)
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(
grouped,
)
}
function hasSanctions (triggers) {
function hasSanctions(triggers) {
return _.some(_.matches({ requirement: 'sanctions' }))(triggers)
}
function maxDaysThreshold (triggers) {
function maxDaysThreshold(triggers) {
return _.max(_.map('thresholdDays')(triggers))
}
function getCashLimit (triggers) {
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
function getCashLimit(triggers) {
const withFiat = _.filter(({ triggerType }) =>
_.includes(triggerType, ['txVolume', 'txAmount']),
)
const blocking = _.filter(({ requirement }) =>
_.includes(requirement, ['block', 'suspend']),
)
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
}
const hasRequirement = requirement => _.compose(_.negate(_.isEmpty), _.find(_.matches({ requirement })))
const hasRequirement = requirement =>
_.compose(_.negate(_.isEmpty), _.find(_.matches({ requirement })))
const hasPhone = hasRequirement('sms')
const hasFacephoto = hasRequirement('facephoto')
@ -28,7 +37,16 @@ const hasIdScan = hasRequirement('idCardData')
const AUTH_METHODS = {
SMS: 'SMS',
EMAIL: 'EMAIL'
EMAIL: 'EMAIL',
}
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }
module.exports = {
getBackwardsCompatibleTriggers,
hasSanctions,
maxDaysThreshold,
getCashLimit,
hasPhone,
hasFacephoto,
hasIdScan,
AUTH_METHODS,
}

View file

@ -5,75 +5,87 @@ const logger = require('./logger')
const db = require('./db')
const ofac = require('./ofac/index')
function logSanctionsMatch (deviceId, customer, sanctionsId, alias) {
function logSanctionsMatch(deviceId, customer, sanctionsId, alias) {
const sql = `insert into sanctions_logs
(id, device_id, sanctioned_id, sanctioned_alias_id, sanctioned_alias_full_name, customer_id)
values
($1, $2, $3, $4, $5, $6)`
return db.none(sql, [uuid.v4(), deviceId, sanctionsId, alias.id, alias.fullName, customer.id])
return db.none(sql, [
uuid.v4(),
deviceId,
sanctionsId,
alias.id,
alias.fullName,
customer.id,
])
}
function logSanctionsMatches (deviceId, customer, results) {
const logAlias = resultId => alias => logSanctionsMatch(deviceId, customer, resultId, alias)
function logSanctionsMatches(deviceId, customer, results) {
const logAlias = resultId => alias =>
logSanctionsMatch(deviceId, customer, resultId, alias)
const logResult = result => _.map(logAlias(result.id), result.aliases)
return Promise.all(_.flatMap(logResult, results))
}
function matchOfac (deviceId, customer) {
return Promise.resolve()
.then(() => {
// Probably because we haven't asked for ID yet
if (!_.isPlainObject(customer.idCardData)) {
return true
}
function matchOfac(deviceId, customer) {
return Promise.resolve().then(() => {
// Probably because we haven't asked for ID yet
if (!_.isPlainObject(customer.idCardData)) {
return true
}
const nameParts = {
firstName: customer.idCardData.firstName,
lastName: customer.idCardData.lastName
}
const nameParts = {
firstName: customer.idCardData.firstName,
lastName: customer.idCardData.lastName,
}
if (_.some(_.isNil, _.values(nameParts))) {
logger.error(new Error(`Insufficient idCardData while matching OFAC for: ${customer.id}`))
return true
}
if (_.some(_.isNil, _.values(nameParts))) {
logger.error(
new Error(
`Insufficient idCardData while matching OFAC for: ${customer.id}`,
),
)
return true
}
const birthDate = customer.idCardData.dateOfBirth
const birthDate = customer.idCardData.dateOfBirth
if (_.isNil(birthDate)) {
logger.error(new Error(`No birth date while matching OFAC for: ${customer.id}`))
return true
}
if (_.isNil(birthDate)) {
logger.error(
new Error(`No birth date while matching OFAC for: ${customer.id}`),
)
return true
}
const options = {
threshold: 0.85,
fullNameThreshold: 0.95,
debug: false
}
const options = {
threshold: 0.85,
fullNameThreshold: 0.95,
debug: false,
}
const results = ofac.match(nameParts, birthDate, options)
const results = ofac.match(nameParts, birthDate, options)
return logSanctionsMatches(deviceId, customer, results)
.then(() => !_.isEmpty(results))
})
return logSanctionsMatches(deviceId, customer, results).then(
() => !_.isEmpty(results),
)
})
}
function validateOfac (deviceId, customer) {
function validateOfac(deviceId, customer) {
if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false)
if (customer.sanctionsOverride === 'verified') return Promise.resolve(true)
return matchOfac(deviceId, customer)
.then(didMatch => !didMatch)
return matchOfac(deviceId, customer).then(didMatch => !didMatch)
}
function validationPatch (deviceId, customer) {
return validateOfac(deviceId, customer)
.then(sanctions =>
_.isNil(customer.sanctions) || customer.sanctions !== sanctions ?
{ sanctions } :
{}
)
function validationPatch(deviceId, customer) {
return validateOfac(deviceId, customer).then(sanctions =>
_.isNil(customer.sanctions) || customer.sanctions !== sanctions
? { sanctions }
: {},
)
}
module.exports = {validationPatch}
module.exports = { validationPatch }

View file

@ -11,7 +11,7 @@ const uuid = require('uuid')
*
* @returns {object} Newly created compliance override
*/
function add (complianceOverride) {
function add(complianceOverride) {
const sql = `insert into compliance_overrides
(id,
customer_id,
@ -25,7 +25,7 @@ function add (complianceOverride) {
complianceOverride.customerId,
complianceOverride.complianceType,
complianceOverride.overrideBy,
complianceOverride.verification
complianceOverride.verification,
])
}

View file

@ -10,43 +10,43 @@ const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HO
const anonymousCustomer = {
uuid: '47ac1184-8102-11e7-9079-8f13a7117867',
name: 'anonymous'
name: 'anonymous',
}
const CASH_UNIT_CAPACITY = {
default: {
cashbox: 600,
cassette: 500
cassette: 500,
},
douro: {
cashbox: 600,
cassette: 500
cassette: 500,
},
grandola: {
cashbox: 2000,
recycler: 2800
recycler: 2800,
},
aveiro: {
cashbox: 1500,
recycler: 60,
cassette: 500
cassette: 500,
},
tejo: {
// TODO: add support for the different cashbox configuration in Tejo
cashbox: 1000,
cassette: 500
cassette: 500,
},
gaia: {
cashbox: 600
cashbox: 600,
},
sintra: {
cashbox: 1000,
cassette: 500
cassette: 500,
},
gmuk1: {
cashbox: 2200,
cassette: 2000
}
cassette: 2000,
},
}
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
@ -69,7 +69,7 @@ const WALLET_SCORE_THRESHOLD = 9
const BALANCE_FETCH_SPEED_MULTIPLIER = {
NORMAL: 1,
SLOW: 3
SLOW: 3,
}
module.exports = {
@ -90,5 +90,5 @@ module.exports = {
WALLET_SCORE_THRESHOLD,
RECEIPT,
PSQL_URL,
BALANCE_FETCH_SPEED_MULTIPLIER
BALANCE_FETCH_SPEED_MULTIPLIER,
}

View file

@ -5,7 +5,9 @@ const db = require('./db')
const getCustomerNotes = customerId => {
const sql = `SELECT * FROM customer_notes WHERE customer_id=$1`
return db.oneOrNone(sql, [customerId]).then(res => _.mapKeys((_, key) => _.camelize(key), res))
return db
.oneOrNone(sql, [customerId])
.then(res => _.mapKeys((_, key) => _.camelize(key), res))
}
const createCustomerNote = (customerId, userId, title, content) => {
@ -27,5 +29,5 @@ module.exports = {
getCustomerNotes,
createCustomerNote,
deleteCustomerNote,
updateCustomerNote
updateCustomerNote,
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
const db = require('../lib/db')
const logger = require('./logger')
const upsert = 'insert into migrations (id, data) values (1, $1) on conflict (id) do update set data = $1'
const upsert =
'insert into migrations (id, data) values (1, $1) on conflict (id) do update set data = $1'
function DbMigrateStore () {
}
function DbMigrateStore() {}
DbMigrateStore.prototype.save = function (set, fn) {
let insertData = JSON.stringify({
lastRun: set.lastRun,
migrations: set.migrations
migrations: set.migrations,
})
db.none(upsert, [insertData]).then(fn).catch(logger.error)
}

View file

@ -16,9 +16,8 @@ const pgp = Pgp({
else if (e.query) {
logger.error(e.query)
e.params && logger.error(e.params)
}
else logger.error(err)
}
} else logger.error(err)
},
})
const db = pgp(PSQL_URL)

View file

@ -1,25 +1,25 @@
const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
function sendMessage(settings, rec) {
return Promise.resolve().then(() => {
const pluginCode =
settings.config.notifications_thirdParty_email || 'mailgun'
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
return plugin.sendMessage(account, rec)
})
}
function sendCustomerMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
function sendCustomerMessage(settings, rec) {
return Promise.resolve().then(() => {
const pluginCode =
settings.config.notifications_thirdParty_email || 'mailgun'
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
return plugin.sendMessage(account, rec)
})
}
module.exports = {sendMessage, sendCustomerMessage}
module.exports = { sendMessage, sendCustomerMessage }

View file

@ -5,11 +5,11 @@ const isProdMode = () => process.env.NODE_ENV === 'production'
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
function isRemoteNode (crypto) {
function isRemoteNode(crypto) {
return process.env[`${crypto.cryptoCode}_NODE_LOCATION`] === 'remote'
}
function isRemoteWallet (crypto) {
function isRemoteWallet(crypto) {
return process.env[`${crypto.cryptoCode}_WALLET_LOCATION`] === 'remote'
}
@ -17,5 +17,5 @@ module.exports = {
isDevMode,
isProdMode,
isRemoteNode,
isRemoteWallet
isRemoteWallet,
}

View file

@ -15,7 +15,7 @@ const E = function (name) {
module.exports = E
function register (errorName) {
function register(errorName) {
E[errorName] = E(errorName)
}

View file

@ -5,7 +5,7 @@ const _ = require('lodash/fp')
const subscriptions = {}
function subscribe (eventType, callback) {
function subscribe(eventType, callback) {
const id = uuid.v1()
if (!subscriptions[eventType]) subscriptions[eventType] = {}
@ -15,15 +15,18 @@ function subscribe (eventType, callback) {
return {
unsubscribe: () => {
delete subscriptions[eventType][id]
if (_.keys(subscriptions[eventType]).length === 0) delete subscriptions[eventType]
}
if (_.keys(subscriptions[eventType]).length === 0)
delete subscriptions[eventType]
},
}
}
function publish (eventType, arg) {
function publish(eventType, arg) {
if (!subscriptions[eventType]) return
_.keys(subscriptions[eventType]).forEach(key => subscriptions[eventType][key](arg))
_.keys(subscriptions[eventType]).forEach(key =>
subscriptions[eventType][key](arg),
)
}
module.exports = { subscribe, publish }

View file

@ -6,71 +6,80 @@ const ccxt = require('./plugins/exchange/ccxt')
const mockExchange = require('./plugins/exchange/mock-exchange')
const accounts = require('./new-admin/config/accounts')
function lookupExchange (settings, cryptoCode) {
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
function lookupExchange(settings, cryptoCode) {
const exchange = configManager.getWalletSettings(
cryptoCode,
settings.config,
).exchange
if (exchange === 'no-exchange') return null
return exchange
}
function fetchExchange (settings, cryptoCode) {
return Promise.resolve()
.then(() => {
const exchangeName = lookupExchange(settings, cryptoCode)
if (exchangeName === 'mock-exchange') return { exchangeName, account: { currencyMarket: 'EUR' } }
if (!exchangeName) throw new Error('No exchange set')
const account = settings.accounts[exchangeName]
function fetchExchange(settings, cryptoCode) {
return Promise.resolve().then(() => {
const exchangeName = lookupExchange(settings, cryptoCode)
if (exchangeName === 'mock-exchange')
return { exchangeName, account: { currencyMarket: 'EUR' } }
if (!exchangeName) throw new Error('No exchange set')
const account = settings.accounts[exchangeName]
return { exchangeName, account }
})
return { exchangeName, account }
})
}
function buy (settings, tradeEntry) {
function buy(settings, tradeEntry) {
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
return fetchExchange(settings, cryptoCode)
.then(r => {
if (r.exchangeName === 'mock-exchange') {
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
}
return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName)
})
return fetchExchange(settings, cryptoCode).then(r => {
if (r.exchangeName === 'mock-exchange') {
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
}
return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName)
})
}
function sell (settings, tradeEntry) {
function sell(settings, tradeEntry) {
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
return fetchExchange(settings, cryptoCode)
.then(r => {
if (r.exchangeName === 'mock-exchange') {
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
}
return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName)
})
return fetchExchange(settings, cryptoCode).then(r => {
if (r.exchangeName === 'mock-exchange') {
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
}
return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName)
})
}
function active (settings, cryptoCode) {
function active(settings, cryptoCode) {
return !!lookupExchange(settings, cryptoCode)
}
function getMarkets () {
const filterExchanges = _.filter(it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange')
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
function getMarkets() {
const filterExchanges = _.filter(
it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange',
)
const availableExchanges = _.map(
it => it.code,
filterExchanges(accounts.ACCOUNT_LIST),
)
const fetchMarketForExchange = exchange =>
ccxt.getMarkets(exchange, ALL_CRYPTOS)
ccxt
.getMarkets(exchange, ALL_CRYPTOS)
.then(markets => ({ exchange, markets }))
.catch(error => ({
exchange,
markets: [],
error: error.message
error: error.message,
}))
const transformToObject = _.reduce((acc, { exchange, markets }) => ({
...acc,
[exchange]: markets
}), {})
const transformToObject = _.reduce(
(acc, { exchange, markets }) => ({
...acc,
[exchange]: markets,
}),
{},
)
const promises = _.map(fetchMarketForExchange, availableExchanges)
return Promise.all(promises)
.then(transformToObject)
return Promise.all(promises).then(transformToObject)
}
module.exports = {
@ -78,5 +87,5 @@ module.exports = {
buy,
sell,
active,
getMarkets
getMarkets,
}

View file

@ -7,34 +7,52 @@ const T = require('./time')
const MAX_ROTATIONS = 5
const _getFiatRates = () => (
axios.get('https://bitpay.com/api/rates')
.then(response => response.data)
)
const _getFiatRates = () =>
axios.get('https://bitpay.com/api/rates').then(response => response.data)
const getFiatRates = mem(_getFiatRates, {
maxAge: 6 * T.hours,
cacheKey: () => ''
cacheKey: () => '',
})
const API_QUEUE = [
{ api: getBitPayFxRate, name: 'bitpay', fiatCodeProperty: 'code', rateProperty: 'rate' }
{
api: getBitPayFxRate,
name: 'bitpay',
fiatCodeProperty: 'code',
rateProperty: 'rate',
},
]
function getBitPayFxRate (fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty) {
return getFiatRates()
.then(({ data: fxRates }) => {
const defaultFiatRate = findCurrencyRates(fxRates, defaultFiatMarket, fiatCodeProperty, rateProperty)
const fxRate = findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty).div(defaultFiatRate)
return {
fxRate
}
})
function getBitPayFxRate(
fiatCode,
defaultFiatMarket,
fiatCodeProperty,
rateProperty,
) {
return getFiatRates().then(({ data: fxRates }) => {
const defaultFiatRate = findCurrencyRates(
fxRates,
defaultFiatMarket,
fiatCodeProperty,
rateProperty,
)
const fxRate = findCurrencyRates(
fxRates,
fiatCode,
fiatCodeProperty,
rateProperty,
).div(defaultFiatRate)
return {
fxRate,
}
})
}
function findCurrencyRates (fxRates, fiatCode, fiatCodeProperty, rateProperty) {
function findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty) {
const rates = _.find(_.matchesProperty(fiatCodeProperty, fiatCode), fxRates)
if (!rates || !rates[rateProperty]) throw new Error(`Unsupported currency: ${fiatCode}`)
if (!rates || !rates[rateProperty])
throw new Error(`Unsupported currency: ${fiatCode}`)
return new BN(rates[rateProperty].toString())
}
@ -46,15 +64,20 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => {
if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`)
return activeAPI(fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty)
.catch(() => {
// Switch service
const erroredService = API_QUEUE.shift()
API_QUEUE.push(erroredService)
if (retries >= MAX_ROTATIONS) throw new Error(`FOREX API error from ${erroredService.name}`)
return activeAPI(
fiatCode,
defaultFiatMarket,
fiatCodeProperty,
rateProperty,
).catch(() => {
// Switch service
const erroredService = API_QUEUE.shift()
API_QUEUE.push(erroredService)
if (retries >= MAX_ROTATIONS)
throw new Error(`FOREX API error from ${erroredService.name}`)
return getRate(++retries, fiatCode)
})
return getRate(++retries, fiatCode)
})
}
module.exports = { getFiatRates, getRate }

View file

@ -4,7 +4,10 @@ const nmd = require('nano-markdown')
const plugins = require('../plugins')
const configManager = require('../new-config-manager')
const settingsLoader = require('../new-settings-loader')
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
const {
batchGetCustomInfoRequest,
getCustomInfoRequests,
} = require('../new-admin/services/customInfoRequests')
const state = require('../middlewares/state')
const { getMachine } = require('../machine-loader')
@ -14,23 +17,26 @@ const urlsToPing = [
`us.archive.ubuntu.com`,
`uk.archive.ubuntu.com`,
`za.archive.ubuntu.com`,
`cn.archive.ubuntu.com`
`cn.archive.ubuntu.com`,
]
const speedtestFiles = [
{
url: 'https://github.com/lamassu/speed-test-assets/raw/main/python-defaults_2.7.18-3.tar.gz',
size: 44668
}
size: 44668,
},
]
const addSmthInfo = (dstField, srcFields) => smth =>
(smth && smth.active) ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
const addOperatorInfo = addSmthInfo(
'operatorInfo',
['name', 'phone', 'email', 'website', 'companyNumber']
)
const addOperatorInfo = addSmthInfo('operatorInfo', [
'name',
'phone',
'email',
'website',
'companyNumber',
])
const addReceiptInfo = receiptInfo => ret => {
if (!receiptInfo) return ret
@ -56,87 +62,90 @@ const addReceiptInfo = receiptInfo => ret => {
_.pick(fields),
)(receiptInfo)
return (receiptInfo.paper || receiptInfo.sms) ?
_.set('receiptInfo', receiptInfo, ret) :
ret
return receiptInfo.paper || receiptInfo.sms
? _.set('receiptInfo', receiptInfo, ret)
: ret
}
const addMachineScreenOpts = smth => _.update(
'screenOptions',
_.flow(
addSmthInfo(
'rates',
[
'active'
]
)(smth.rates)
const addMachineScreenOpts = smth =>
_.update(
'screenOptions',
_.flow(addSmthInfo('rates', ['active'])(smth.rates)),
)
)
/* TODO: Simplify this. */
const buildTriggers = allTriggers => {
const normalTriggers = []
const customTriggers = _.filter(o => {
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId))
normalTriggers.push(o)
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
}, allTriggers)
return _.flow(
_.map(_.get('customInfoRequestId')),
batchGetCustomInfoRequest
)(customTriggers)
.then(res => {
res.forEach((details, index) => {
// make sure we aren't attaching the details to the wrong trigger
if (customTriggers[index].customInfoRequestId !== details.id) return
customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
})
return [...normalTriggers, ...customTriggers]
batchGetCustomInfoRequest,
)(customTriggers).then(res => {
res.forEach((details, index) => {
// make sure we aren't attaching the details to the wrong trigger
if (customTriggers[index].customInfoRequestId !== details.id) return
customTriggers[index] = {
...customTriggers[index],
customInfoRequest: details,
}
})
return [...normalTriggers, ...customTriggers]
})
}
const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => {
const massageCoins = _.map(_.pick([
'batchable',
'cashInCommission',
'cashInFee',
'cashOutCommission',
'cashOutFee',
'cryptoCode',
'cryptoCodeDisplay',
'cryptoNetwork',
'cryptoUnits',
'display',
'minimumTx',
'isCashInOnly'
]))
const staticConfig = ({
currentConfigVersion,
deviceId,
deviceName,
pq,
settings,
}) => {
const massageCoins = _.map(
_.pick([
'batchable',
'cashInCommission',
'cashInFee',
'cashOutCommission',
'cashOutFee',
'cryptoCode',
'cryptoCodeDisplay',
'cryptoNetwork',
'cryptoUnits',
'display',
'minimumTx',
'isCashInOnly',
]),
)
const staticConf = _.flow(
_.pick([
'coins',
'configVersion',
'timezone',
'screenOptions'
]),
_.pick(['coins', 'configVersion', 'timezone', 'screenOptions']),
_.update('coins', massageCoins),
_.set('serverVersion', VERSION),
)(pq)
return Promise.all([
!!configManager.getCompliance(settings.config).enablePaperWalletOnly,
configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config),
configManager.getTriggersAutomation(
getCustomInfoRequests(true),
settings.config,
),
buildTriggers(configManager.getTriggers(settings.config)),
configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2',
configManager.getWalletSettings('BTC', settings.config).layer2 !==
'no-layer2',
configManager.getLocale(deviceId, settings.config),
configManager.getOperatorInfo(settings.config),
configManager.getReceipt(settings.config),
configManager.getAllMachineScreenOpts(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active,
getMachine(deviceId, currentConfigVersion),
configManager.getCustomerAuthenticationMethod(settings.config)
])
.then(([
configManager.getCustomerAuthenticationMethod(settings.config),
]).then(
([
enablePaperWalletOnly,
triggersAutomation,
triggers,
@ -149,88 +158,121 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
{ numberOfCassettes, numberOfRecyclers },
customerAuthentication,
]) =>
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
null :
_.flow(
_.assign({
enablePaperWalletOnly,
triggersAutomation,
triggers,
hasLightning,
localeInfo: {
country: localeInfo.country,
languages: localeInfo.languages,
fiatCode: localeInfo.fiatCurrency
},
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers },
twoWayMode,
customerAuthentication,
speedtestFiles,
urlsToPing,
}),
addOperatorInfo(operatorInfo),
addReceiptInfo(receiptInfo),
addMachineScreenOpts(machineScreenOpts)
)(staticConf))
currentConfigVersion && currentConfigVersion >= staticConf.configVersion
? null
: _.flow(
_.assign({
enablePaperWalletOnly,
triggersAutomation,
triggers,
hasLightning,
localeInfo: {
country: localeInfo.country,
languages: localeInfo.languages,
fiatCode: localeInfo.fiatCurrency,
},
machineInfo: {
deviceId,
deviceName,
numberOfCassettes,
numberOfRecyclers,
},
twoWayMode,
customerAuthentication,
speedtestFiles,
urlsToPing,
}),
addOperatorInfo(operatorInfo),
addReceiptInfo(receiptInfo),
addMachineScreenOpts(machineScreenOpts),
)(staticConf),
)
}
const setZeroConfLimit = config => coin =>
_.set(
'zeroConfLimit',
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit ?? 0,
coin
coin,
)
const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings }) => {
const massageCassettes = cassettes =>
cassettes ?
_.flow(
cassettes => _.set('physical', _.get('cassettes', cassettes), cassettes),
cassettes => _.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
_.unset('cassettes'),
_.unset('virtualCassettes')
)(cassettes) :
null
cassettes
? _.flow(
cassettes =>
_.set('physical', _.get('cassettes', cassettes), cassettes),
cassettes =>
_.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
_.unset('cassettes'),
_.unset('virtualCassettes'),
)(cassettes)
: null
const massageRecyclers = recyclers =>
recyclers ?
_.flow(
recyclers => _.set('physical', _.get('recyclers', recyclers), recyclers),
recyclers => _.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
_.unset('recyclers'),
_.unset('virtualRecyclers')
)(recyclers) :
null
recyclers
? _.flow(
recyclers =>
_.set('physical', _.get('recyclers', recyclers), recyclers),
recyclers =>
_.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
_.unset('recyclers'),
_.unset('virtualRecyclers'),
)(recyclers)
: null
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
state.pids = _.update(
operatorId,
_.set(deviceId, { pid, ts: Date.now() }),
state.pids,
)
const res = _.flow(
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'recyclers', 'coins', 'rates']),
_.pick([
'areThereAvailablePromoCodes',
'balances',
'cassettes',
'recyclers',
'coins',
'rates',
]),
_.update('cassettes', massageCassettes),
_.update('recyclers', massageRecyclers),
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
_.update(
'coins',
_.map(({ cryptoCode, rates }) => [cryptoCode, rates]),
),
/* [{ cryptoCode: balance }, ...] => [[cryptoCode, { balance }], ...] */
_.update('balances', _.flow(
_.toPairs,
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }])
)),
_.update(
'balances',
_.flow(
_.toPairs,
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }]),
),
),
/* Group the separate objects by cryptoCode */
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
({ areThereAvailablePromoCodes, balances, cassettes, recyclers, coins, rates }) => ({
({
areThereAvailablePromoCodes,
balances,
cassettes,
recyclers,
coins,
rates,
}) => ({
areThereAvailablePromoCodes,
cassettes,
recyclers,
coins: _.flow(
_.reduce(
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
rates
rates,
),
/* { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } => [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] */
@ -240,17 +282,36 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
_.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)),
/* Only send coins which have all information needed by the machine. This prevents the machine going down if there's an issue with the coin node */
_.filter(coin => ['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(it => it in coin))
)(_.concat(balances, coins))
_.filter(coin =>
['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(
it => it in coin,
),
),
)(_.concat(balances, coins)),
}),
_.update('coins', _.map(setZeroConfLimit(settings.config))),
_.set('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
_.set('restartServices', !!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid),
_.set('emptyUnit', !!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid),
_.set('refillUnit', !!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid),
_.set('diagnostics', !!pid && state.diagnostics?.[operatorId]?.[deviceId] === pid),
_.set(
'shutdown',
!!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid,
),
_.set(
'restartServices',
!!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid,
),
_.set(
'emptyUnit',
!!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid,
),
_.set(
'refillUnit',
!!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid,
),
_.set(
'diagnostics',
!!pid && state.diagnostics?.[operatorId]?.[deviceId] === pid,
),
)(pq)
// Clean up the state middleware and prevent commands from being issued more than once
@ -269,8 +330,11 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
return res
}
const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) =>
const configs = (
parent,
{ currentConfigVersion },
{ deviceId, deviceName, operatorId, pid, settings },
) =>
plugins(settings, deviceId)
.pollQueries()
.then(pq => ({
@ -290,15 +354,17 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera
}),
}))
const massageTerms = terms => (terms.active && terms.text) ? ({
tcPhoto: Boolean(terms.tcPhoto),
delay: Boolean(terms.delay),
title: terms.title,
text: nmd(terms.text),
accept: terms.acceptButtonText,
cancel: terms.cancelButtonText,
}) : null
const massageTerms = terms =>
terms.active && terms.text
? {
tcPhoto: Boolean(terms.tcPhoto),
delay: Boolean(terms.delay),
title: terms.title,
text: nmd(terms.text),
accept: terms.acceptButtonText,
cancel: terms.cancelButtonText,
}
: null
/*
* The type of the result of `configManager.getTermsConditions()` is more or
@ -327,7 +393,7 @@ const massageTerms = terms => (terms.active && terms.text) ? ({
* If the `hash` differs from `currentHash` then everything is resent (to
* simplify machine implementation).
*/
const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settings }, info) => {
const terms = (parent, { currentConfigVersion, currentHash }, { settings }) => {
const isNone = x => _.isNil(x) || _.isEmpty(x)
let latestTerms = configManager.getTermsConditions(settings.config)
@ -342,17 +408,22 @@ const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settin
const isHashNew = hash !== currentHash
const text = isHashNew ? latestTerms.text : null
return settingsLoader.fetchCurrentConfigVersion()
return settingsLoader
.fetchCurrentConfigVersion()
.catch(() => null)
.then(configVersion => isHashNew || _.isNil(currentConfigVersion) || currentConfigVersion < configVersion)
.then(isVersionNew => isVersionNew ? _.omit(['text'], latestTerms) : null)
.then(
configVersion =>
isHashNew ||
_.isNil(currentConfigVersion) ||
currentConfigVersion < configVersion,
)
.then(isVersionNew => (isVersionNew ? _.omit(['text'], latestTerms) : null))
.then(details => ({ hash, details, text }))
}
module.exports = {
Query: {
configs,
terms,
}
},
}

View file

@ -5,11 +5,11 @@ const { ApolloServer } = require('@apollo/server')
const devMode = !!require('minimist')(process.argv.slice(2)).dev
const context = ({ req, res }) => ({
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
deviceName: req.deviceName, /* lib/middlewares/authorize.js */
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
deviceId: req.deviceId /* lib/middlewares/populateDeviceId.js */,
deviceName: req.deviceName /* lib/middlewares/authorize.js */,
operatorId: res.locals.operatorId /* lib/middlewares/operatorId.js */,
pid: req.query.pid,
settings: req.settings, /* lib/middlewares/populateSettings.js */
settings: req.settings /* lib/middlewares/populateSettings.js */,
})
const graphQLServer = new ApolloServer({
@ -21,7 +21,7 @@ const graphQLServer = new ApolloServer({
return error
},
includeStacktraceInErrorResponses: devMode,
logger
logger,
})
module.exports = { graphQLServer, context }
module.exports = { graphQLServer, context }

View file

@ -1,234 +1,234 @@
const gql = require('graphql-tag')
module.exports = gql`
type Coin {
cryptoCode: String!
cryptoCodeDisplay: String!
display: String!
minimumTx: String!
cashInFee: String!
cashOutFee: String!
cashInCommission: String!
cashOutCommission: String!
cryptoNetwork: String!
cryptoUnits: String!
batchable: Boolean!
isCashInOnly: Boolean!
}
type Coin {
cryptoCode: String!
cryptoCodeDisplay: String!
display: String!
minimumTx: String!
cashInFee: String!
cashOutFee: String!
cashInCommission: String!
cashOutCommission: String!
cryptoNetwork: String!
cryptoUnits: String!
batchable: Boolean!
isCashInOnly: Boolean!
}
type LocaleInfo {
country: String!
fiatCode: String!
languages: [String!]!
}
type LocaleInfo {
country: String!
fiatCode: String!
languages: [String!]!
}
type OperatorInfo {
name: String!
phone: String!
email: String!
website: String!
companyNumber: String!
}
type OperatorInfo {
name: String!
phone: String!
email: String!
website: String!
companyNumber: String!
}
type MachineInfo {
deviceId: String! @deprecated(reason: "unused by the machine")
deviceName: String
numberOfCassettes: Int
numberOfRecyclers: Int
}
type MachineInfo {
deviceId: String! @deprecated(reason: "unused by the machine")
deviceName: String
numberOfCassettes: Int
numberOfRecyclers: Int
}
type ReceiptInfo {
paper: Boolean!
automaticPrint: Boolean!
sms: Boolean!
operatorWebsite: Boolean!
operatorEmail: Boolean!
operatorPhone: Boolean!
companyNumber: Boolean!
machineLocation: Boolean!
customerNameOrPhoneNumber: Boolean!
exchangeRate: Boolean!
addressQRCode: Boolean!
}
type ReceiptInfo {
paper: Boolean!
automaticPrint: Boolean!
sms: Boolean!
operatorWebsite: Boolean!
operatorEmail: Boolean!
operatorPhone: Boolean!
companyNumber: Boolean!
machineLocation: Boolean!
customerNameOrPhoneNumber: Boolean!
exchangeRate: Boolean!
addressQRCode: Boolean!
}
type MachineScreenOptions {
rates: RateScreenOptions!
}
type MachineScreenOptions {
rates: RateScreenOptions!
}
type RateScreenOptions {
active: Boolean!
}
type RateScreenOptions {
active: Boolean!
}
type SpeedtestFile {
url: String!
size: Int!
}
type SpeedtestFile {
url: String!
size: Int!
}
enum TriggerAutomationType {
Automatic
Manual
}
enum TriggerAutomationType {
Automatic
Manual
}
type CustomTriggersAutomation {
id: ID!
type: TriggerAutomationType!
}
type CustomTriggersAutomation {
id: ID!
type: TriggerAutomationType!
}
type TriggersAutomation {
sanctions: TriggerAutomationType!
idCardPhoto: TriggerAutomationType!
idCardData: TriggerAutomationType!
facephoto: TriggerAutomationType!
usSsn: TriggerAutomationType!
custom: [CustomTriggersAutomation]!
}
type TriggersAutomation {
sanctions: TriggerAutomationType!
idCardPhoto: TriggerAutomationType!
idCardData: TriggerAutomationType!
facephoto: TriggerAutomationType!
usSsn: TriggerAutomationType!
custom: [CustomTriggersAutomation]!
}
type CustomScreen {
text: String!
title: String!
}
type CustomScreen {
text: String!
title: String!
}
type CustomInput {
type: String!
constraintType: String!
label1: String
label2: String
choiceList: [String]
}
type CustomInput {
type: String!
constraintType: String!
label1: String
label2: String
choiceList: [String]
}
type CustomRequest {
name: String!
input: CustomInput!
screen1: CustomScreen!
screen2: CustomScreen!
}
type CustomRequest {
name: String!
input: CustomInput!
screen1: CustomScreen!
screen2: CustomScreen!
}
type CustomInfoRequest {
id: String!
enabled: Boolean!
customRequest: CustomRequest!
}
type CustomInfoRequest {
id: String!
enabled: Boolean!
customRequest: CustomRequest!
}
type Trigger {
id: String!
direction: String!
requirement: String!
triggerType: String!
type Trigger {
id: String!
direction: String!
requirement: String!
triggerType: String!
suspensionDays: Float
threshold: Int
thresholdDays: Int
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
customInfoRequest: CustomInfoRequest
externalService: String
}
suspensionDays: Float
threshold: Int
thresholdDays: Int
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
customInfoRequest: CustomInfoRequest
externalService: String
}
type TermsDetails {
tcPhoto: Boolean!
delay: Boolean!
title: String!
accept: String!
cancel: String!
}
type TermsDetails {
tcPhoto: Boolean!
delay: Boolean!
title: String!
accept: String!
cancel: String!
}
type Terms {
hash: String!
text: String
details: TermsDetails
}
type Terms {
hash: String!
text: String
details: TermsDetails
}
enum CustomerAuthentication {
EMAIL
SMS
}
enum CustomerAuthentication {
EMAIL
SMS
}
type StaticConfig {
configVersion: Int!
type StaticConfig {
configVersion: Int!
coins: [Coin!]!
enablePaperWalletOnly: Boolean!
hasLightning: Boolean!
serverVersion: String!
timezone: Int!
twoWayMode: Boolean!
customerAuthentication: CustomerAuthentication!
coins: [Coin!]!
enablePaperWalletOnly: Boolean!
hasLightning: Boolean!
serverVersion: String!
timezone: Int!
twoWayMode: Boolean!
customerAuthentication: CustomerAuthentication!
localeInfo: LocaleInfo!
operatorInfo: OperatorInfo
machineInfo: MachineInfo!
receiptInfo: ReceiptInfo
screenOptions: MachineScreenOptions
localeInfo: LocaleInfo!
operatorInfo: OperatorInfo
machineInfo: MachineInfo!
receiptInfo: ReceiptInfo
screenOptions: MachineScreenOptions
speedtestFiles: [SpeedtestFile!]!
urlsToPing: [String!]!
speedtestFiles: [SpeedtestFile!]!
urlsToPing: [String!]!
triggersAutomation: TriggersAutomation!
triggers: [Trigger!]!
}
triggersAutomation: TriggersAutomation!
triggers: [Trigger!]!
}
type DynamicCoinValues {
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
# However, it can be used to generate the cache key, if we ever move to an
# actual caching mechanism.
#timestamp: String!
type DynamicCoinValues {
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
# However, it can be used to generate the cache key, if we ever move to an
# actual caching mechanism.
#timestamp: String!
cryptoCode: String!
balance: String!
cryptoCode: String!
balance: String!
# Raw rates
ask: String!
bid: String!
# Raw rates
ask: String!
bid: String!
# Rates with commissions applied
cashIn: String!
cashOut: String!
# Rates with commissions applied
cashIn: String!
cashOut: String!
zeroConfLimit: Int!
}
zeroConfLimit: Int!
}
type PhysicalCassette {
name: String!
denomination: Int!
count: Int!
}
type PhysicalCassette {
name: String!
denomination: Int!
count: Int!
}
type PhysicalRecycler {
name: String!
number: Int!
denomination: Int!
count: Int!
}
type PhysicalRecycler {
name: String!
number: Int!
denomination: Int!
count: Int!
}
type Cassettes {
physical: [PhysicalCassette!]!
virtual: [Int!]!
}
type Cassettes {
physical: [PhysicalCassette!]!
virtual: [Int!]!
}
type Recyclers {
physical: [PhysicalRecycler!]!
virtual: [Int!]!
}
type Recyclers {
physical: [PhysicalRecycler!]!
virtual: [Int!]!
}
type DynamicConfig {
areThereAvailablePromoCodes: Boolean!
cassettes: Cassettes
recyclers: Recyclers
coins: [DynamicCoinValues!]!
reboot: Boolean!
shutdown: Boolean!
restartServices: Boolean!
emptyUnit: Boolean!
refillUnit: Boolean!
diagnostics: Boolean!
}
type DynamicConfig {
areThereAvailablePromoCodes: Boolean!
cassettes: Cassettes
recyclers: Recyclers
coins: [DynamicCoinValues!]!
reboot: Boolean!
shutdown: Boolean!
restartServices: Boolean!
emptyUnit: Boolean!
refillUnit: Boolean!
diagnostics: Boolean!
}
type Configs {
static: StaticConfig
dynamic: DynamicConfig!
}
type Configs {
static: StaticConfig
dynamic: DynamicConfig!
}
type Query {
configs(currentConfigVersion: Int): Configs!
terms(currentHash: String, currentConfigVersion: Int): Terms
}
type Query {
configs(currentConfigVersion: Int): Configs!
terms(currentHash: String, currentConfigVersion: Int): Terms
}
`

View file

@ -2,27 +2,27 @@ const uuid = require('uuid')
const db = require('./db')
function createHardwareCredential (userID, credentialData) {
function createHardwareCredential(userID, credentialData) {
const sql = `INSERT INTO hardware_credentials (id, user_id, data) VALUES ($1, $2, $3)`
return db.none(sql, [uuid.v4(), userID, credentialData])
}
function getHardwareCredentials () {
function getHardwareCredentials() {
const sql = `SELECT * FROM hardware_credentials`
return db.any(sql)
}
function getHardwareCredentialsByUserId (userID) {
function getHardwareCredentialsByUserId(userID) {
const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1`
return db.any(sql, [userID])
}
function getUserByUserHandle (userHandle) {
function getUserByUserHandle(userHandle) {
const sql = `SELECT users.id, users.username, users.role FROM users INNER JOIN hardware_credentials hc ON users.id=hc.user_id WHERE data->>'userHandle'=$1::jsonb::text`
return db.oneOrNone(sql, [userHandle])
}
function updateHardwareCredential (credential) {
function updateHardwareCredential(credential) {
const sql = `UPDATE hardware_credentials SET last_used=now(), data=$1 WHERE id=$2`
return db.none(sql, [credential.data, credential.id])
}
@ -32,5 +32,5 @@ module.exports = {
getHardwareCredentials,
getHardwareCredentialsByUserId,
getUserByUserHandle,
updateHardwareCredential
updateHardwareCredential,
}

View file

@ -2,38 +2,47 @@ const configManager = require('./new-config-manager')
const ph = require('./plugin-helper')
const _ = require('lodash/fp')
function fetch (settings, cryptoCode) {
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
function fetch(settings, cryptoCode) {
const plugin = configManager.getWalletSettings(
cryptoCode,
settings.config,
).layer2
if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve()
const layer2 = ph.load(ph.LAYER2, plugin)
const account = settings.accounts[plugin]
return Promise.resolve({layer2, account})
return Promise.resolve({ layer2, account })
}
function newAddress (settings, info) {
return fetch(settings, info.cryptoCode)
.then(r => {
if (!r) return
return r.layer2.newAddress(r.account, info)
})
function newAddress(settings, info) {
return fetch(settings, info.cryptoCode).then(r => {
if (!r) return
return r.layer2.newAddress(r.account, info)
})
}
function getStatus (settings, tx) {
function getStatus(settings, tx) {
const toAddress = tx.layer2Address
if (!toAddress) return Promise.resolve({status: 'notSeen'})
if (!toAddress) return Promise.resolve({ status: 'notSeen' })
return fetch(settings, tx.cryptoCode)
.then(r => {
if (!r) return {status: 'notSeen'}
return r.layer2.getStatus(r.account, toAddress, tx.cryptoAtoms, tx.cryptoCode)
})
return fetch(settings, tx.cryptoCode).then(r => {
if (!r) return { status: 'notSeen' }
return r.layer2.getStatus(
r.account,
toAddress,
tx.cryptoAtoms,
tx.cryptoCode,
)
})
}
function cryptoNetwork (settings, cryptoCode) {
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
function cryptoNetwork(settings, cryptoCode) {
const plugin = configManager.getWalletSettings(
cryptoCode,
settings.config,
).layer2
const layer2 = ph.load(ph.LAYER2, plugin)
const account = settings.accounts[plugin]
@ -41,7 +50,7 @@ function cryptoNetwork (settings, cryptoCode) {
return layer2.cryptoNetwork(account, cryptoCode)
}
function isLayer2Address (address) {
function isLayer2Address(address) {
return address.split(':').length >= 2
}
@ -49,5 +58,5 @@ module.exports = {
isLayer2Address,
newAddress,
getStatus,
cryptoNetwork
cryptoNetwork,
}

View file

@ -7,40 +7,42 @@ const LOG_LEVEL = process.env.LOG_LEVEL
const logger = new winston.Logger({
level: LOG_LEVEL,
transports: [
new (winston.transports.Console)({
new winston.transports.Console({
timestamp: true,
colorize: true,
handleExceptions: true,
humanReadableUnhandledException: true
humanReadableUnhandledException: true,
}),
new Postgres({
connectionString: PSQL_URL,
tableName: 'server_logs',
handleExceptions: true,
humanReadableUnhandledException: true
})
humanReadableUnhandledException: true,
}),
],
rewriters: [
(...[,, 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
method: meta.config?.method,
}
}
return meta instanceof Error ? { message: meta.message, stack: meta.stack, meta } : meta
}
return meta instanceof Error
? { message: meta.message, stack: meta.stack, meta }
: meta
},
],
exitOnError: false
exitOnError: false,
})
logger.stream = {
write: message => {
logger.info(message.trim())
}
},
}
module.exports = logger

View file

@ -7,7 +7,6 @@ const logger = require('./logger')
const pgp = require('pg-promise')()
const getMachineName = require('./machine-loader').getMachineName
const NUM_RESULTS = 500
/**
* Get the latest log's timestamp
@ -20,12 +19,15 @@ const NUM_RESULTS = 500
*
* @returns {date} Last timestamp
*/
function getLastSeen (deviceId) {
function getLastSeen(deviceId) {
const sql = `select id, timestamp, serial from logs
where device_id=$1
order by timestamp desc, serial desc limit 1`
return db.oneOrNone(sql, [deviceId])
.then(log => log ? {timestamp: log.timestamp, serial: log.serial, id: log.id} : null)
return db
.oneOrNone(sql, [deviceId])
.then(log =>
log ? { timestamp: log.timestamp, serial: log.serial, id: log.id } : null,
)
}
/**
@ -40,10 +42,11 @@ function getLastSeen (deviceId) {
*
* @returns {null}
*/
function update (deviceId, logLines) {
const cs = new pgp.helpers.ColumnSet([
'id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'],
{table: 'logs'})
function update(deviceId, logLines) {
const cs = new pgp.helpers.ColumnSet(
['id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'],
{ table: 'logs' },
)
const logs = _.map(log => {
const formatted = {
@ -52,7 +55,7 @@ function update (deviceId, logLines) {
message: log.msg,
logLevel: _.contains('error', _.lowerCase(log.msg)) ? 'error' : 'info',
timestamp: log.timestamp,
serial: log.serial || 0
serial: log.serial || 0,
}
return _.mapKeys(_.snakeCase, formatted)
}, logLines)
@ -61,7 +64,7 @@ function update (deviceId, logLines) {
return db.none(sql)
}
function clearOldLogs () {
function clearOldLogs() {
const sqls = `delete from logs
where timestamp < now() - interval '3 days';
delete from server_logs
@ -69,7 +72,7 @@ function clearOldLogs () {
return db.multi(sqls)
}
function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
function getUnlimitedMachineLogs(deviceId, until = new Date().toISOString()) {
// Note: sql is a little confusing here, since timestamp is used both as a column
// and a reserved word, but it works.
const sql = `select id, log_level, timestamp, message from logs
@ -78,14 +81,21 @@ function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
and timestamp > (timestamp $2 - interval '2 days')
order by timestamp desc, serial desc`
return Promise.all([db.any(sql, [ deviceId, until ]), getMachineName(deviceId)])
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
return Promise.all([
db.any(sql, [deviceId, until]),
getMachineName(deviceId),
]).then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: { deviceId, name: machineName },
}))
}
function getMachineLogs (deviceId, until = new Date().toISOString(), limit = null, offset = 0) {
function getMachineLogs(
deviceId,
until = new Date().toISOString(),
limit = null,
offset = 0,
) {
const sql = `select id, log_level, timestamp, message from logs
where device_id=$1
and timestamp <= $2
@ -93,14 +103,22 @@ function getMachineLogs (deviceId, until = new Date().toISOString(), limit = nul
limit $3
offset $4`
return Promise.all([db.any(sql, [ deviceId, until, limit, offset ]), getMachineName(deviceId)])
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
return Promise.all([
db.any(sql, [deviceId, until, limit, offset]),
getMachineName(deviceId),
]).then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: { deviceId, name: machineName },
}))
}
function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
function simpleGetMachineLogs(
deviceId,
from = new Date(0).toISOString(),
until = new Date().toISOString(),
limit = null,
offset = 0,
) {
const sql = `select id, log_level, timestamp, message from logs
where device_id=$1
and timestamp >= $2
@ -109,31 +127,38 @@ function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until
limit $4
offset $5`
return db.any(sql, [ deviceId, from, until, limit, offset ])
return db
.any(sql, [deviceId, from, until, limit, offset])
.then(_.map(_.mapKeys(_.camelCase)))
}
function logDateFormat (timezone, logs, fields) {
function logDateFormat(timezone, logs, fields) {
return _.map(log => {
const values = _.map(
field =>
{
if (_.isNil(log[field])) return null
if (!isValid(log[field])) {
logger.warn(`Tried to convert to ${timezone} timezone the value ${log[field]} and failed. Returning original value...`)
return log[field]
}
const date = utcToZonedTime(timezone, log[field])
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
},
fields
)
const values = _.map(field => {
if (_.isNil(log[field])) return null
if (!isValid(log[field])) {
logger.warn(
`Tried to convert to ${timezone} timezone the value ${log[field]} and failed. Returning original value...`,
)
return log[field]
}
const date = utcToZonedTime(timezone, log[field])
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
}, fields)
const fieldsToOverride = _.zipObject(fields, values)
return {
...log,
...fieldsToOverride
...fieldsToOverride,
}
}, logs)
}
module.exports = { getUnlimitedMachineLogs, getMachineLogs, simpleGetMachineLogs, update, getLastSeen, clearOldLogs, logDateFormat }
module.exports = {
getUnlimitedMachineLogs,
getMachineLogs,
simpleGetMachineLogs,
update,
getLastSeen,
clearOldLogs,
logDateFormat,
}

View file

@ -1,62 +1,66 @@
const db = require('./db')
const uuid = require('uuid')
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
function getAvailablePromoCodes () {
function getAvailablePromoCodes() {
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
return db.any(sql)
}
function getPromoCode (code) {
function getPromoCode(code) {
const sql = `SELECT * FROM coupons WHERE code=$1 AND soft_deleted=false`
return db.oneOrNone(sql, [code])
}
function createPromoCode (code, discount) {
function createPromoCode(code, discount) {
const sql = `INSERT INTO coupons (id, code, discount) VALUES ($1, $2, $3) RETURNING *`
return db.one(sql, [uuid.v4(), code, discount])
}
function deletePromoCode (id) {
function deletePromoCode(id) {
const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1`
return db.none(sql, [id])
}
function getNumberOfAvailablePromoCodes () {
function getNumberOfAvailablePromoCodes() {
const sql = `SELECT COUNT(id) FROM coupons WHERE soft_deleted=false`
return db.one(sql).then(res => res.count)
}
function getAvailableIndividualDiscounts () {
function getAvailableIndividualDiscounts() {
const sql = `SELECT * FROM individual_discounts WHERE soft_deleted=false`
return db.any(sql).then(res => _.map(it => ({
id: it.id,
customerId: it.customer_id,
discount: it.discount
}), res))
return db.any(sql).then(res =>
_.map(
it => ({
id: it.id,
customerId: it.customer_id,
discount: it.discount,
}),
res,
),
)
}
function getCustomerActiveIndividualDiscount (customerId) {
function getCustomerActiveIndividualDiscount(customerId) {
const sql = `SELECT * FROM individual_discounts WHERE customer_id=$1 AND soft_deleted=false LIMIT 1`
return db.oneOrNone(sql, [customerId]).then(res => {
if (!_.isNil(res)) {
return {
id: res.id,
customerId: res.customer_id,
discount: res.discount
discount: res.discount,
}
}
return res
})
}
function createIndividualDiscount (customerId, discount) {
function createIndividualDiscount(customerId, discount) {
const sql = `INSERT INTO individual_discounts (id, customer_id, discount) VALUES ($1, $2, $3)`
return db.none(sql, [uuid.v4(), customerId, discount])
}
function deleteIndividualDiscount (id) {
function deleteIndividualDiscount(id) {
const sql = `UPDATE individual_discounts SET soft_deleted=true WHERE id=$1`
return db.none(sql, [id])
}
@ -70,5 +74,5 @@ module.exports = {
getAvailableIndividualDiscounts,
getCustomerActiveIndividualDiscount,
createIndividualDiscount,
deleteIndividualDiscount
deleteIndividualDiscount,
}

View file

@ -13,7 +13,7 @@ const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const notifierUtils = require('./notifier/utils')
const notifierQueries = require('./notifier/queries')
const { GraphQLError } = require('graphql');
const { GraphQLError } = require('graphql')
const { loadLatestConfig } = require('./new-settings-loader')
const logger = require('./logger')
@ -37,7 +37,7 @@ select d.*, COALESCE(emptybills, 0) + COALESCE(regularbills, 0) as cashbox from
group by cit.device_id
) as nbills on nbills.device_id = d.device_id`
function toMachineObject (r) {
function toMachineObject(r) {
return {
deviceId: r.device_id,
cashUnits: {
@ -51,48 +51,56 @@ function toMachineObject (r) {
recycler3: r.recycler3,
recycler4: r.recycler4,
recycler5: r.recycler5,
recycler6: r.recycler6
recycler6: r.recycler6,
},
numberOfCassettes: r.number_of_cassettes,
numberOfRecyclers: r.number_of_recyclers,
version: r.version,
model: r.model,
diagnostics: {
timestamp: r.diagnostics_timestamp? new Date(r.diagnostics_timestamp) : null,
scanTimestamp: r.diagnostics_scan_timestamp? new Date(r.diagnostics_scan_timestamp) : null,
frontTimestamp: r.diagnostics_front_timestamp? new Date(r.diagnostics_front_timestamp) : null
timestamp: r.diagnostics_timestamp
? new Date(r.diagnostics_timestamp)
: null,
scanTimestamp: r.diagnostics_scan_timestamp
? new Date(r.diagnostics_scan_timestamp)
: null,
frontTimestamp: r.diagnostics_front_timestamp
? new Date(r.diagnostics_front_timestamp)
: null,
},
pairedAt: new Date(r.created),
lastPing: new Date(r.last_online),
name: r.name,
paired: r.paired
paired: r.paired,
// TODO: we shall start using this JSON field at some point
// location: r.location,
}
}
function getMachineIds () {
function getMachineIds() {
const sql = 'select device_id from devices'
return db.any(sql)
}
function getMachines () {
function getMachines() {
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} where display=TRUE ORDER BY created`
return db.any(sql)
.then(rr => rr.map(toMachineObject))
return db.any(sql).then(rr => rr.map(toMachineObject))
}
function getUnpairedMachines () {
return db.any('SELECT * FROM unpaired_devices')
.then(_.map(r =>
_.flow(
_.set('deviceId', _.get('device_id', r)),
_.unset('device_id')
)(r)
))
function getUnpairedMachines() {
return db
.any('SELECT * FROM unpaired_devices')
.then(
_.map(r =>
_.flow(
_.set('deviceId', _.get('device_id', r)),
_.unset('device_id'),
)(r),
),
)
}
function getConfig (defaultConfig) {
function getConfig(defaultConfig) {
return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig()
}
@ -104,7 +112,7 @@ const getStatus = (ping, stuck) => {
return fullyFunctionalStatus
}
function addName (pings, events, config) {
function addName(pings, events, config) {
return machine => {
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
@ -113,22 +121,38 @@ function addName (pings, events, config) {
const statuses = [
getStatus(
_.first(pings[machine.deviceId]),
_.first(checkStuckScreen(events, machine))
)
_.first(checkStuckScreen(events, machine)),
),
]
return _.assign(machine, { cashOut, statuses })
}
}
function getMachineNames (config) {
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
))
function getMachineNames(config) {
return Promise.all([
getMachines(),
getConfig(config),
getNetworkHeartbeat(),
getNetworkPerformance(),
])
.then(([rawMachines, config, heartbeat, performance]) =>
Promise.all([
rawMachines,
checkPings(rawMachines),
dbm.machineEvents(),
config,
heartbeat,
performance,
]),
)
.then(([rawMachines, pings, events, config, heartbeat, performance]) => {
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
const mergeByDeviceId = (x, y) =>
_.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(
mergeByDeviceId(rawMachines, heartbeat),
performance,
)
return machines.map(addName(pings, events, config))
})
@ -144,67 +168,133 @@ function getMachineNames (config) {
* @param {string} machineId machine id
* @returns {string} machine name
*/
function getMachineName (machineId) {
function getMachineName(machineId) {
const sql = 'SELECT name FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId])
.then(it => it?.name)
return db.oneOrNone(sql, [machineId]).then(it => it?.name)
}
function getMachine (machineId, config) {
function getMachine(machineId, config) {
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
if (r === null) throw new GraphQLError('Resource doesn\'t exist', { extensions: { code: 'NOT_FOUND' } })
if (r === null)
throw new GraphQLError("Resource doesn't exist", {
extensions: { code: 'NOT_FOUND' },
})
else return toMachineObject(r)
})
return Promise.all([queryMachine, dbm.machineEvents(), config, getNetworkHeartbeatByDevice(machineId), getNetworkPerformanceByDevice(machineId)])
.then(([machine, events, config, heartbeat, performance]) => {
const pings = checkPings([machine])
const mergedMachine = {
...machine,
responseTime: _.get('responseTime', heartbeat),
packetLoss: _.get('packetLoss', heartbeat),
downloadSpeed: _.get('downloadSpeed', performance),
}
return Promise.all([
queryMachine,
dbm.machineEvents(),
config,
getNetworkHeartbeatByDevice(machineId),
getNetworkPerformanceByDevice(machineId),
]).then(([machine, events, config, heartbeat, performance]) => {
const pings = checkPings([machine])
const mergedMachine = {
...machine,
responseTime: _.get('responseTime', heartbeat),
packetLoss: _.get('packetLoss', heartbeat),
downloadSpeed: _.get('downloadSpeed', performance),
}
return addName(pings, events, config)(mergedMachine)
})
return addName(pings, events, config)(mergedMachine)
})
}
function renameMachine (rec) {
function renameMachine(rec) {
const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
return db.none(sql, [rec.newName, rec.deviceId])
}
function resetCashOutBills (rec) {
function resetCashOutBills(rec) {
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
const { cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits
const {
cassette1,
cassette2,
cassette3,
cassette4,
recycler1,
recycler2,
recycler3,
recycler4,
recycler5,
recycler6,
} = rec.cashUnits
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11;`
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
return db
.none(sql, [
cassette1,
cassette2,
cassette3,
cassette4,
recycler1,
recycler2,
recycler3,
recycler4,
recycler5,
recycler6,
rec.deviceId,
])
.then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
}
function setCassetteBills (rec) {
const { cashbox, cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits
return getMachine(rec.deviceId)
.then(machine => {
const oldCashboxCount = machine?.cashUnits?.cashbox
if (_.isNil(oldCashboxCount) || cashbox.toString() === oldCashboxCount.toString()) {
const sql = `
function setCassetteBills(rec) {
const {
cashbox,
cassette1,
cassette2,
cassette3,
cassette4,
recycler1,
recycler2,
recycler3,
recycler4,
recycler5,
recycler6,
} = rec.cashUnits
return getMachine(rec.deviceId).then(machine => {
const oldCashboxCount = machine?.cashUnits?.cashbox
if (
_.isNil(oldCashboxCount) ||
cashbox.toString() === oldCashboxCount.toString()
) {
const sql = `
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6)
WHERE device_id=$11`
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId])
}
return db.none(sql, [
cassette1,
cassette2,
cassette3,
cassette4,
recycler1,
recycler2,
recycler3,
recycler4,
recycler5,
recycler6,
rec.deviceId,
])
}
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue: oldCashboxCount })
return batching.updateMachineWithBatch({
...rec,
oldCashboxValue: oldCashboxCount,
})
})
}
function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
function emptyMachineUnits({ deviceId, newUnits, fiatCode }) {
return loadLatestConfig()
.then(config => Promise.all([getMachine(deviceId), configManager.getCashOut(deviceId, config)]))
.then(config =>
Promise.all([
getMachine(deviceId),
configManager.getCashOut(deviceId, config),
]),
)
.then(([machine, cashoutSettings]) => {
const movedBills = _.reduce(
(acc, value) => ({
@ -212,34 +302,42 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
[value]: {
operationName: `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`,
delta: newUnits[value] - machine.cashUnits[value],
denomination: value !== 'cashbox' ? cashoutSettings[value] : null
}
denomination: value !== 'cashbox' ? cashoutSettings[value] : null,
},
}),
{},
_.keys(newUnits)
_.keys(newUnits),
)
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills)))
const operationNames = _.mapValues(it => it.operationName)(
_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills)),
)
const operationsToCreate = _.map(it => ({
id: uuid.v4(),
device_id: deviceId,
operation_type: it
operation_type: it,
}))(operationNames)
const billArr = _.reduce(
(acc, value) => {
const unit = movedBills[value]
return _.concat(acc, _.times(() => ({
id: uuid.v4(),
fiat: unit.denomination,
fiat_code: fiatCode,
device_id: deviceId
// TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id
}), Math.abs(unit.delta)))
return _.concat(
acc,
_.times(
() => ({
id: uuid.v4(),
fiat: unit.denomination,
fiat_code: fiatCode,
device_id: deviceId,
// TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id
}),
Math.abs(unit.delta),
),
)
},
[],
_.keys(_.omit(['cashbox'], movedBills))
_.keys(_.omit(['cashbox'], movedBills)),
)
// This occurs when an empty unit is called when the units are already empty, hence, no bills moved around
@ -249,59 +347,76 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
return db.tx(t => {
const q1Cols = ['id', 'device_id', 'operation_type']
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
const q1 = t.none(
pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'),
)
const q2Cols = ['id', 'fiat', 'fiat_code', 'device_id']
const q2 = t.none(pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'))
const q3 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
deviceId
])
const q2 = t.none(
pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'),
)
const q3 = t.none(
`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`,
[
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
deviceId,
],
)
return t.batch([q1, q2, q3])
})
})
}
function refillMachineUnits ({ deviceId, newUnits }) {
return getMachine(deviceId)
.then(machine => {
const movedBills = _.reduce(
(acc, value) => ({
...acc,
[value]: {
operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`,
delta: newUnits[value] - machine.cashUnits[value]
}
}),
{},
_.keys(newUnits)
function refillMachineUnits({ deviceId, newUnits }) {
return getMachine(deviceId).then(machine => {
const movedBills = _.reduce(
(acc, value) => ({
...acc,
[value]: {
operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`,
delta: newUnits[value] - machine.cashUnits[value],
},
}),
{},
_.keys(newUnits),
)
const operationNames = _.mapValues(it => it.operationName)(
_.filter(it => Math.abs(it.delta) > 0)(
_.omit(
['cassette1', 'cassette2', 'cassette3', 'cassette4'],
movedBills,
),
),
)
const operationsToCreate = _.map(it => ({
id: uuid.v4(),
device_id: deviceId,
operation_type: it,
}))(operationNames)
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
if (_.isEmpty(operationsToCreate)) {
return Promise.resolve()
}
return db.tx(t => {
const q1Cols = ['id', 'device_id', 'operation_type']
const q1 = t.none(
pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'),
)
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills)))
const operationsToCreate = _.map(it => ({
id: uuid.v4(),
device_id: deviceId,
operation_type: it
}))(operationNames)
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
if (_.isEmpty(operationsToCreate)) {
return Promise.resolve()
}
return db.tx(t => {
const q1Cols = ['id', 'device_id', 'operation_type']
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
const q2 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [
const q2 = t.none(
`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`,
[
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
@ -312,64 +427,70 @@ function refillMachineUnits ({ deviceId, newUnits }) {
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
deviceId
])
return t.batch([q1, q2])
})
deviceId,
],
)
return t.batch([q1, q2])
})
})
}
function unpair (rec) {
function unpair(rec) {
return pairing.unpair(rec.deviceId)
}
function reboot (rec) {
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
function reboot(rec) {
return db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'reboot',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)])
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
])
}
function shutdown (rec) {
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
function shutdown(rec) {
return db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'shutdown',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)])
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
])
}
function restartServices (rec) {
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
function restartServices(rec) {
return db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'restartServices',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)])
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
])
}
function emptyUnit (rec) {
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
function emptyUnit(rec) {
return db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'emptyUnit',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)])
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
])
}
function refillUnit (rec) {
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
function refillUnit(rec) {
return db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'refillUnit',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)])
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
])
}
function diagnostics (rec) {
function diagnostics(rec) {
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${rec.deviceId}/`
const sql = `UPDATE devices
SET diagnostics_timestamp = NULL,
@ -391,48 +512,65 @@ function diagnostics (rec) {
return Promise.all(removeFiles)
.then(() => db.none(sql, [rec.deviceId]))
.then(() => db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
{
action: 'diagnostics',
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
}
)]))
.then(() =>
db.none('NOTIFY $1:name, $2', [
'machineAction',
JSON.stringify({
action: 'diagnostics',
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
}),
]),
)
}
function setMachine (rec, operatorId) {
function setMachine(rec, operatorId) {
rec.operatorId = operatorId
switch (rec.action) {
case 'rename': return renameMachine(rec)
case 'resetCashOutBills': return resetCashOutBills(rec)
case 'setCassetteBills': return setCassetteBills(rec)
case 'unpair': return unpair(rec)
case 'reboot': return reboot(rec)
case 'shutdown': return shutdown(rec)
case 'restartServices': return restartServices(rec)
case 'emptyUnit': return emptyUnit(rec)
case 'refillUnit': return refillUnit(rec)
case 'diagnostics': return diagnostics(rec)
default: throw new Error('No such action: ' + rec.action)
case 'rename':
return renameMachine(rec)
case 'resetCashOutBills':
return resetCashOutBills(rec)
case 'setCassetteBills':
return setCassetteBills(rec)
case 'unpair':
return unpair(rec)
case 'reboot':
return reboot(rec)
case 'shutdown':
return shutdown(rec)
case 'restartServices':
return restartServices(rec)
case 'emptyUnit':
return emptyUnit(rec)
case 'refillUnit':
return refillUnit(rec)
case 'diagnostics':
return diagnostics(rec)
default:
throw new Error('No such action: ' + rec.action)
}
}
function updateNetworkPerformance (deviceId, data) {
function updateNetworkPerformance(deviceId, data) {
if (_.isEmpty(data)) return Promise.resolve(true)
const downloadSpeed = _.head(data)
const dbData = {
device_id: deviceId,
download_speed: downloadSpeed.speed,
created: new Date()
created: new Date(),
}
const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
{ table: 'machine_network_performance' })
const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
const cs = new pgp.helpers.ColumnSet(
['device_id', 'download_speed', 'created'],
{ table: 'machine_network_performance' },
)
const onConflict =
' ON CONFLICT (device_id) DO UPDATE SET ' +
cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
const upsert = pgp.helpers.insert(dbData, cs) + onConflict
return db.none(upsert)
}
function updateNetworkHeartbeat (deviceId, data) {
function updateNetworkHeartbeat(deviceId, data) {
if (_.isEmpty(data)) return Promise.resolve(true)
const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
@ -440,41 +578,47 @@ function updateNetworkHeartbeat (deviceId, data) {
id: uuid.v4(),
device_id: deviceId,
average_response_time: avgResponseTime,
average_packet_loss: avgPacketLoss
average_packet_loss: avgPacketLoss,
}
const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
return db.none(sql)
}
function getNetworkPerformance () {
function getNetworkPerformance() {
const sql = `SELECT device_id, download_speed FROM machine_network_performance`
return db.manyOrNone(sql)
.then(res => _.map(_.mapKeys(_.camelCase))(res))
return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res))
}
function getNetworkHeartbeat () {
function getNetworkHeartbeat() {
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
FROM machine_network_heartbeat
GROUP BY device_id`
return db.manyOrNone(sql)
.then(res => _.map(_.mapKeys(_.camelCase))(res))
return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res))
}
function getNetworkPerformanceByDevice (deviceId) {
function getNetworkPerformanceByDevice(deviceId) {
const sql = `SELECT device_id, download_speed FROM machine_network_performance WHERE device_id = $1`
return db.manyOrNone(sql, [deviceId])
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
return db.manyOrNone(sql, [deviceId]).then(res =>
_.mapKeys(
_.camelCase,
_.find(it => it.device_id === deviceId, res),
),
)
}
function getNetworkHeartbeatByDevice (deviceId) {
function getNetworkHeartbeatByDevice(deviceId) {
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
FROM machine_network_heartbeat WHERE device_id = $1
GROUP BY device_id`
return db.manyOrNone(sql, [deviceId])
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
return db.manyOrNone(sql, [deviceId]).then(res =>
_.mapKeys(
_.camelCase,
_.find(it => it.device_id === deviceId, res),
),
)
}
function updateDiagnostics (deviceId, images) {
function updateDiagnostics(deviceId, images) {
const sql = `UPDATE devices
SET diagnostics_timestamp = NOW(),
diagnostics_scan_updated_at = CASE WHEN $2 THEN NOW() ELSE diagnostics_scan_updated_at END,
@ -484,19 +628,25 @@ function updateDiagnostics (deviceId, images) {
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/`
const { scan, front } = images
return updatePhotos(directory, [['scan.jpg', scan], ['front.jpg', front]])
return updatePhotos(directory, [
['scan.jpg', scan],
['front.jpg', front],
])
.then(() => db.none(sql, [deviceId, !!scan, !!front]))
.catch(err => logger.error('while running machine diagnostics: ', err))
}
const updateFailedQRScans = (deviceId, frames) => {
const timestamp = (new Date()).toISOString()
const timestamp = new Date().toISOString()
const directory = `${OPERATOR_DATA_DIR}/failedQRScans/${deviceId}/`
const filenames = _.map(no => `${timestamp}-${no}.jpg`, _.range(0, _.size(frames)))
const filenames = _.map(
no => `${timestamp}-${no}.jpg`,
_.range(0, _.size(frames)),
)
return updatePhotos(directory, _.zip(filenames, frames))
}
function createPhoto (name, data, dir) {
function createPhoto(name, data, dir) {
if (!data) {
logger.error(`Diagnostics error: No data to save for ${name} photo`)
return Promise.resolve()
@ -507,12 +657,12 @@ function createPhoto (name, data, dir) {
return fsPromises.writeFile(filename, decodedImageData)
}
function updatePhotos (dir, photoPairs) {
function updatePhotos(dir, photoPairs) {
const dirname = path.join(dir)
_.attempt(() => makeDir.sync(dirname))
return Promise.all(photoPairs.map(
([filename, data]) => createPhoto(filename, data, dirname)
))
return Promise.all(
photoPairs.map(([filename, data]) => createPhoto(filename, data, dirname)),
)
}
module.exports = {
@ -530,5 +680,5 @@ module.exports = {
emptyMachineUnits,
refillMachineUnits,
updateDiagnostics,
updateFailedQRScans
updateFailedQRScans,
}

View file

@ -2,7 +2,8 @@ const pairing = require('../pairing')
const logger = require('../logger')
const authorize = function (req, res, next) {
return pairing.isPaired(req.deviceId)
return pairing
.isPaired(req.deviceId)
.then(deviceName => {
if (deviceName) {
req.deviceName = deviceName

View file

@ -1,10 +1,11 @@
const pairing = require('../pairing')
const logger = require('../logger')
function ca (req, res) {
function ca(req, res) {
const token = req.query.token
return pairing.authorizeCaDownload(token)
return pairing
.authorizeCaDownload(token)
.then(ca => res.json({ ca }))
.catch(error => {
logger.error(error.message)

View file

@ -1,9 +1,7 @@
const logger = require('../logger')
function errorHandler (err, req, res, next) {
const statusCode = err.name === 'HTTPError'
? err.code || 500
: 500
function errorHandler(err, req, res) {
const statusCode = err.name === 'HTTPError' ? err.code || 500 : 500
const json = { error: err.message }

View file

@ -5,19 +5,23 @@ const CLOCK_SKEW = 60 * 1000
const REQUEST_TTL = 3 * 60 * 1000
const THROTTLE_CLOCK_SKEW = 60 * 1000
function filterOldRequests (req, res, next) {
function filterOldRequests(req, res, next) {
const deviceTime = req.deviceTime
const deviceId = req.deviceId
const timestamp = Date.now()
const delta = timestamp - Date.parse(deviceTime)
const shouldTrigger = !state.canLogClockSkewMap[deviceId] ||
const shouldTrigger =
!state.canLogClockSkewMap[deviceId] ||
timestamp - state.canLogClockSkewMap[deviceId] >= THROTTLE_CLOCK_SKEW
if (delta > CLOCK_SKEW && shouldTrigger) {
state.canLogClockSkewMap[deviceId] = timestamp
logger.error('Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock',
req.deviceName, (delta / 1000).toFixed(2))
logger.error(
'Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock',
req.deviceName,
(delta / 1000).toFixed(2),
)
}
if (delta > REQUEST_TTL) return res.status(408).json({ error: 'stale' })

View file

@ -1,6 +1,6 @@
const { getOperatorId } = require('../operator')
function findOperatorId (req, res, next) {
function findOperatorId(req, res, next) {
return getOperatorId('middleware')
.then(operatorId => {
res.locals.operatorId = operatorId

View file

@ -1,6 +1,6 @@
const crypto = require('crypto')
function sha256 (buf) {
function sha256(buf) {
if (!buf) return null
const hash = crypto.createHash('sha256')
@ -9,10 +9,13 @@ function sha256 (buf) {
}
const populateDeviceId = function (req, res, next) {
const peerCert = req.socket.getPeerCertificate ? req.socket.getPeerCertificate() : null
const peerCert = req.socket.getPeerCertificate
? req.socket.getPeerCertificate()
: null
const deviceId = peerCert?.raw ? sha256(peerCert.raw) : null
if (!deviceId) return res.status(500).json({ error: 'Unable to find certificate' })
if (!deviceId)
return res.status(500).json({ error: 'Unable to find certificate' })
req.deviceId = deviceId
req.deviceTime = req.get('date')

View file

@ -3,57 +3,74 @@ const state = require('./state')
const newSettingsLoader = require('../new-settings-loader')
const logger = require('../logger')
db.connect({ direct: true }).then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return reload(parsedData.operatorId)
db.connect({ direct: true })
.then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return reload(parsedData.operatorId)
})
return sco.none('LISTEN $1:name', 'reload')
})
return sco.none('LISTEN $1:name', 'reload')
}).catch(console.error)
.catch(console.error)
db.connect({ direct: true }).then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return machineAction(parsedData.action, parsedData.value)
db.connect({ direct: true })
.then(sco => {
sco.client.on('notification', data => {
const parsedData = JSON.parse(data.payload)
return machineAction(parsedData.action, parsedData.value)
})
return sco.none('LISTEN $1:name', 'machineAction')
})
return sco.none('LISTEN $1:name', 'machineAction')
}).catch(console.error)
.catch(console.error)
function machineAction (type, value) {
function machineAction(type, value) {
const deviceId = value.deviceId
const operatorId = value.operatorId
const pid = state.pids?.[operatorId]?.[deviceId]?.pid
switch (type) {
case 'reboot':
logger.debug(`Rebooting machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Rebooting machine '${deviceId}' from operator ${operatorId}`,
)
state.reboots[operatorId] = { [deviceId]: pid }
break
case 'shutdown':
logger.debug(`Shutting down machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Shutting down machine '${deviceId}' from operator ${operatorId}`,
)
state.shutdowns[operatorId] = { [deviceId]: pid }
break
case 'restartServices':
logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Restarting services of machine '${deviceId}' from operator ${operatorId}`,
)
state.restartServicesMap[operatorId] = { [deviceId]: pid }
break
case 'emptyUnit':
logger.debug(`Emptying units from machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Emptying units from machine '${deviceId}' from operator ${operatorId}`,
)
state.emptyUnit[operatorId] = { [deviceId]: pid }
break
case 'refillUnit':
logger.debug(`Refilling recyclers from machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Refilling recyclers from machine '${deviceId}' from operator ${operatorId}`,
)
state.refillUnit[operatorId] = { [deviceId]: pid }
break
case 'diagnostics':
logger.debug(`Running diagnostics on machine '${deviceId}' from operator ${operatorId}`)
logger.debug(
`Running diagnostics on machine '${deviceId}' from operator ${operatorId}`,
)
state.diagnostics[operatorId] = { [deviceId]: pid }
break
default:
break
}
}
function reload (operatorId) {
function reload(operatorId) {
state.needsSettingsReload[operatorId] = true
}
@ -73,11 +90,14 @@ const populateSettings = function (req, res, next) {
// 4. There's no cached config, cache and send the latest config
if (versionId) {
const cachedVersionedSettings = settingsCache.get(`${operatorId}-v${versionId}`)
const cachedVersionedSettings = settingsCache.get(
`${operatorId}-v${versionId}`,
)
if (!cachedVersionedSettings) {
logger.debug('Fetching a specific config version cached value')
return newSettingsLoader.load(versionId)
return newSettingsLoader
.load(versionId)
.then(settings => {
settingsCache.set(`${operatorId}-v${versionId}`, settings)
req.settings = settings
@ -94,16 +114,22 @@ const populateSettings = function (req, res, next) {
const operatorSettings = settingsCache.get(`${operatorId}-latest`)
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
!!needsSettingsReload[operatorId]
? logger.debug('Fetching and caching a new latest config value, as a reload was requested')
: logger.debug('Fetching the latest config version because there\'s no cached value')
needsSettingsReload[operatorId]
? logger.debug(
'Fetching and caching a new latest config value, as a reload was requested',
)
: logger.debug(
"Fetching the latest config version because there's no cached value",
)
return newSettingsLoader.loadLatest()
return newSettingsLoader
.loadLatest()
.then(settings => {
const versionId = settings.version
settingsCache.set(`${operatorId}-latest`, settings)
settingsCache.set(`${operatorId}-v${versionId}`, settings)
if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId]
if (needsSettingsReload[operatorId])
delete needsSettingsReload[operatorId]
req.settings = settings
})
.then(() => next())

View file

@ -12,20 +12,24 @@ const rejectIncompatibleMachines = function (req, res, next) {
const machineMajor = semver.major(machineVersion)
if (serverMajor - machineMajor > 1) {
logger.error(`Machine version too old: ${machineVersion} deviceId: ${deviceId}`)
logger.error(
`Machine version too old: ${machineVersion} deviceId: ${deviceId}`,
)
return res.status(400).json({
error: 'Machine version too old'
error: 'Machine version too old',
})
}
if (serverMajor < machineMajor) {
logger.error(`Machine version too new: ${machineVersion} deviceId: ${deviceId}`)
logger.error(
`Machine version too new: ${machineVersion} deviceId: ${deviceId}`,
)
return res.status(400).json({
error: 'Machine version too new'
error: 'Machine version too new',
})
}
next()
}
module.exports = rejectIncompatibleMachines
module.exports = rejectIncompatibleMachines

View file

@ -7,7 +7,7 @@ module.exports = (function () {
needsSettingsReload: {},
settingsCache: new NodeCache({
stdTTL: SETTINGS_CACHE_REFRESH,
checkperiod: SETTINGS_CACHE_REFRESH // Clear cache every hour
checkperiod: SETTINGS_CACHE_REFRESH, // Clear cache every hour
}),
canLogClockSkewMap: {},
canGetLastSeenMap: {},
@ -18,6 +18,6 @@ module.exports = (function () {
emptyUnit: {},
refillUnit: {},
diagnostics: {},
mnemonic: null
mnemonic: null,
}
}())
})()

View file

@ -7,11 +7,11 @@ const migrateDir = path.resolve(__dirname, '..', 'migrations')
const migrateOpts = {
migrationsDirectory: migrateDir,
stateStore: new DbMigrateStore(),
filterFunction: it => it.match(/^\d+.*\.js$/)
filterFunction: it => it.match(/^\d+.*\.js$/),
}
module.exports = { run }
function run () {
function run() {
return new Promise((resolve, reject) => {
migrate.load(migrateOpts, (err, set) => {
if (err) return reject(err)

View file

@ -1,7 +1,7 @@
const bip39 = require('bip39')
const os = require('os')
function fromSeed (seed) {
function fromSeed(seed) {
const words = bip39.entropyToMnemonic(seed).split(' ')
let mnemonic = ''
@ -11,7 +11,7 @@ function fromSeed (seed) {
return mnemonic
}
function toEntropyBuffer (mnemonic) {
function toEntropyBuffer(mnemonic) {
const hex = bip39.mnemonicToEntropy(mnemonic.split('\n').join(' ').trim())
return Buffer.from(hex.trim(), 'hex')
}

View file

@ -9,8 +9,12 @@ const nocache = require('nocache')
const cookieParser = require('cookie-parser')
const { ApolloServer } = require('@apollo/server')
const { expressMiddleware } = require('@apollo/server/express4')
const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled')
const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default')
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')
@ -23,7 +27,11 @@ const { authDirectiveTransformer } = require('./graphql/directives')
const { typeDefs, resolvers } = require('./graphql/schema')
const findOperatorId = require('../middlewares/operatorId')
const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants')
const { session, cleanUserSessions, buildApolloContext } = require('./middlewares')
const {
session,
cleanUserSessions,
buildApolloContext,
} = require('./middlewares')
const devMode = require('minimist')(process.argv.slice(2)).dev
@ -55,8 +63,12 @@ const loadRoutes = async () => {
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')
const { default: graphqlUploadExpress } = await import(
'graphql-upload/graphqlUploadExpress.mjs'
)
const { default: GraphQLUpload } = await import(
'graphql-upload/GraphQLUpload.mjs'
)
app.use(graphqlUploadExpress())
@ -75,29 +87,33 @@ const loadRoutes = async () => {
return formattedError
},
plugins: [
devMode
? ApolloServerPluginLandingPageLocalDefault()
: ApolloServerPluginLandingPageDisabled()
]
devMode
? ApolloServerPluginLandingPageLocalDefault()
: ApolloServerPluginLandingPageDisabled(),
],
})
await apolloServer.start();
await apolloServer.start()
app.use(
'/graphql',
express.json(),
expressMiddleware(apolloServer, {
context: async ({ req, res }) => buildApolloContext({ req, res })
})
);
context: async ({ req, res }) => buildApolloContext({ req, res }),
}),
)
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false }))
app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false }))
app.use(
'/front-camera-photo',
serveStatic(FRONT_CAMERA_DIR, { index: false }),
)
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
// Everything not on graphql or api/register is redirected to the front-end
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
app.get('*', (req, res) =>
res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')),
)
return app
}
@ -105,10 +121,10 @@ const loadRoutes = async () => {
const certOptions = {
key: fs.readFileSync(KEY_PATH),
cert: fs.readFileSync(CERT_PATH),
ca: fs.readFileSync(CA_PATH)
ca: fs.readFileSync(CA_PATH),
}
async function run () {
async function run() {
const app = await loadRoutes()
const serverPort = devMode ? 8070 : 443

View file

@ -3,8 +3,10 @@ const _ = require('lodash/fp')
const { ALL } = require('../../plugins/common/ccxt')
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } =
COINS
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } =
ALL
const TICKER = 'ticker'
const WALLET = 'wallet'
@ -18,39 +20,142 @@ const WALLET_SCORING = 'wallet_scoring'
const COMPLIANCE = 'compliance'
const ALL_ACCOUNTS = [
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
{ code: 'bitfinex', display: 'Bitfinex', class: EXCHANGE, cryptos: bitfinex.CRYPTO },
{ code: 'binance', display: 'Binance', class: TICKER, cryptos: binance.CRYPTO },
{ code: 'binanceus', display: 'Binance.us', class: TICKER, cryptos: binanceus.CRYPTO },
{
code: 'bitfinex',
display: 'Bitfinex',
class: TICKER,
cryptos: bitfinex.CRYPTO,
},
{
code: 'bitfinex',
display: 'Bitfinex',
class: EXCHANGE,
cryptos: bitfinex.CRYPTO,
},
{
code: 'binance',
display: 'Binance',
class: TICKER,
cryptos: binance.CRYPTO,
},
{
code: 'binanceus',
display: 'Binance.us',
class: TICKER,
cryptos: binanceus.CRYPTO,
},
{ code: 'cex', display: 'CEX.IO', class: TICKER, cryptos: cex.CRYPTO },
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.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: '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: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
{ code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT, USDC] },
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] },
{
code: 'no-layer2',
display: 'No Layer 2',
class: LAYER_2,
cryptos: ALL_CRYPTOS,
},
{
code: 'infura',
display: 'Infura/Alchemy',
class: WALLET,
cryptos: [ETH, USDT, USDC],
},
{
code: 'trongrid',
display: 'Trongrid',
class: WALLET,
cryptos: [TRX, USDT_TRON],
},
{
code: 'geth',
display: 'geth (deprecated)',
class: WALLET,
cryptos: [ETH, USDT, USDC],
},
{ 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: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
{
code: 'bitcoincashd',
display: 'bitcoincashd',
class: WALLET,
cryptos: [BCH],
},
{
code: 'bitgo',
display: 'BitGo',
class: WALLET,
cryptos: [BTC, ZEC, LTC, BCH, DASH],
},
{ code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] },
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
{
code: 'bitstamp',
display: 'Bitstamp',
class: EXCHANGE,
cryptos: bitstamp.CRYPTO,
},
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO },
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO },
{ code: 'binance', display: 'Binance', class: EXCHANGE, cryptos: binance.CRYPTO },
{ code: 'binanceus', display: 'Binance.us', class: EXCHANGE, cryptos: binanceus.CRYPTO },
{
code: 'kraken',
display: 'Kraken',
class: EXCHANGE,
cryptos: kraken.CRYPTO,
},
{
code: 'binance',
display: 'Binance',
class: EXCHANGE,
cryptos: binance.CRYPTO,
},
{
code: 'binanceus',
display: 'Binance.us',
class: EXCHANGE,
cryptos: binanceus.CRYPTO,
},
{ code: 'cex', display: 'CEX.IO', class: EXCHANGE, cryptos: cex.CRYPTO },
{ code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS },
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
{
code: 'mock-wallet',
display: 'Mock (Caution!)',
class: WALLET,
cryptos: ALL_CRYPTOS,
dev: true,
},
{
code: 'no-exchange',
display: 'No exchange',
class: EXCHANGE,
cryptos: ALL_CRYPTOS,
},
{
code: 'mock-exchange',
display: 'Mock exchange',
class: EXCHANGE,
cryptos: ALL_CRYPTOS,
dev: true,
},
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
{
code: 'mock-id-verify',
display: 'Mock ID verifier',
class: ID_VERIFIER,
dev: true,
},
{ code: 'twilio', display: 'Twilio', class: SMS },
{ code: 'telnyx', display: 'Telnyx', class: SMS },
{ code: 'vonage', display: 'Vonage', class: SMS },
@ -58,17 +163,51 @@ const ALL_ACCOUNTS = [
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ 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: '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, USDC, USDT_TRON, TRX, ZEC] },
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
{
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: '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, USDC, USDT_TRON, TRX, ZEC],
},
{
code: 'mock-scoring',
display: 'Mock scoring',
class: WALLET_SCORING,
cryptos: ALL_CRYPTOS,
dev: true,
},
{ 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,
},
]
const flags = require('minimist')(process.argv.slice(2))
const devMode = flags.dev || flags.lamassuDev
const ACCOUNT_LIST = devMode ? ALL_ACCOUNTS : _.filter(it => !it.dev)(ALL_ACCOUNTS)
const ACCOUNT_LIST = devMode
? ALL_ACCOUNTS
: _.filter(it => !it.dev)(ALL_ACCOUNTS)
module.exports = { ACCOUNT_LIST }

File diff suppressed because one or more lines are too long

View file

@ -1,255 +1,266 @@
{
"attribute": {"name":0, "nativeName":1},
"rtl": {"ar":1,"dv":1,"fa":1,"ha":1,"he":1,"ks":1,"ku":1,"ps":1,"ur":1,"yi":1},
"lang": {
"aa":["Afar","Afar"],
"ab":["Abkhazian","Аҧсуа"],
"af":["Afrikaans","Afrikaans"],
"ak":["Akan","Akana"],
"am":["Amharic","አማርኛ"],
"an":["Aragonese","Aragonés"],
"ar":["Arabic","العربية"],
"as":["Assamese","অসমীয়া"],
"av":["Avar","Авар"],
"ay":["Aymara","Aymar"],
"az":["Azerbaijani","Azərbaycanca / آذربايجان"],
"ba":["Bashkir","Башҡорт"],
"be":["Belarusian","Беларуская"],
"bg":["Bulgarian","Български"],
"bh":["Bihari","भोजपुरी"],
"bi":["Bislama","Bislama"],
"bm":["Bambara","Bamanankan"],
"bn":["Bengali","বাংলা"],
"bo":["Tibetan","བོད་ཡིག / Bod skad"],
"br":["Breton","Brezhoneg"],
"bs":["Bosnian","Bosanski"],
"ca":["Catalan","Català"],
"ce":["Chechen","Нохчийн"],
"ch":["Chamorro","Chamoru"],
"co":["Corsican","Corsu"],
"cr":["Cree","Nehiyaw"],
"cs":["Czech","Česky"],
"cu":["Old Church Slavonic / Old Bulgarian","словѣньскъ / slověnĭskŭ"],
"cv":["Chuvash","Чăваш"],
"cy":["Welsh","Cymraeg"],
"da":["Danish","Dansk"],
"de":["German","Deutsch"],
"dv":["Divehi","ދިވެހިބަސް"],
"dz":["Dzongkha","ཇོང་ཁ"],
"ee":["Ewe","Ɛʋɛ"],
"el":["Greek","Ελληνικά"],
"en":["English","English"],
"eo":["Esperanto","Esperanto"],
"es":["Spanish","Español"],
"et":["Estonian","Eesti"],
"eu":["Basque","Euskara"],
"fa":["Persian","فارسی"],
"ff":["Peul","Fulfulde"],
"fi":["Finnish","Suomi"],
"fj":["Fijian","Na Vosa Vakaviti"],
"fo":["Faroese","Føroyskt"],
"fr":["French","Français"],
"fy":["West Frisian","Frysk"],
"ga":["Irish","Gaeilge"],
"gd":["Scottish Gaelic","Gàidhlig"],
"gl":["Galician","Galego"],
"gn":["Guarani","Avañe'ẽ"],
"gu":["Gujarati","ગુજરાતી"],
"gv":["Manx","Gaelg"],
"ha":["Hausa","هَوُسَ"],
"he":["Hebrew","עברית"],
"hi":["Hindi","हिन्दी"],
"ho":["Hiri Motu","Hiri Motu"],
"hr":["Croatian","Hrvatski"],
"ht":["Haitian","Krèyol ayisyen"],
"hu":["Hungarian","Magyar"],
"hy":["Armenian","Հայերեն"],
"hz":["Herero","Otsiherero"],
"ia":["Interlingua","Interlingua"],
"id":["Indonesian","Bahasa Indonesia"],
"ie":["Interlingue","Interlingue"],
"ig":["Igbo","Igbo"],
"ii":["Sichuan Yi","ꆇꉙ / 四川彝语"],
"ik":["Inupiak","Iñupiak"],
"io":["Ido","Ido"],
"is":["Icelandic","Íslenska"],
"it":["Italian","Italiano"],
"iu":["Inuktitut","ᐃᓄᒃᑎᑐᑦ"],
"ja":["Japanese","日本語"],
"jv":["Javanese","Basa Jawa"],
"ka":["Georgian","ქართული"],
"kg":["Kongo","KiKongo"],
"ki":["Kikuyu","Gĩkũyũ"],
"kj":["Kuanyama","Kuanyama"],
"kk":["Kazakh","Қазақша"],
"kl":["Greenlandic","Kalaallisut"],
"km":["Cambodian","ភាសាខ្មែរ"],
"kn":["Kannada","ಕನ್ನಡ"],
"ko":["Korean","한국어"],
"kr":["Kanuri","Kanuri"],
"ks":["Kashmiri","कश्मीरी / كشميري"],
"ku":["Kurdish","Kurdî / كوردی"],
"kv":["Komi","Коми"],
"kw":["Cornish","Kernewek"],
"ky":["Kirghiz","Kırgızca / Кыргызча"],
"la":["Latin","Latina"],
"lb":["Luxembourgish","Lëtzebuergesch"],
"lg":["Ganda","Luganda"],
"li":["Limburgian","Limburgs"],
"ln":["Lingala","Lingála"],
"lo":["Laotian","ລາວ / Pha xa lao"],
"lt":["Lithuanian","Lietuvių"],
"lv":["Latvian","Latviešu"],
"mg":["Malagasy","Malagasy"],
"mh":["Marshallese","Kajin Majel / Ebon"],
"mi":["Maori","Māori"],
"mk":["Macedonian","Македонски"],
"ml":["Malayalam","മലയാളം"],
"mn":["Mongolian","Монгол"],
"mo":["Moldovan","Moldovenească"],
"mr":["Marathi","मराठी"],
"ms":["Malay","Bahasa Melayu"],
"mt":["Maltese","bil-Malti"],
"my":["Burmese","Myanmasa"],
"na":["Nauruan","Dorerin Naoero"],
"nd":["North Ndebele","Sindebele"],
"ne":["Nepali","नेपाली"],
"ng":["Ndonga","Oshiwambo"],
"nl":["Dutch","Nederlands"],
"nn":["Norwegian Nynorsk","Norsk (nynorsk)"],
"no":["Norwegian","Norsk (bokmål / riksmål)"],
"nr":["South Ndebele","isiNdebele"],
"nv":["Navajo","Diné bizaad"],
"ny":["Chichewa","Chi-Chewa"],
"oc":["Occitan","Occitan"],
"oj":["Ojibwa","ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
"om":["Oromo","Oromoo"],
"or":["Oriya","ଓଡ଼ିଆ"],
"os":["Ossetian / Ossetic","Иронау"],
"pa":["Panjabi / Punjabi","ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
"pi":["Pali","Pāli / पाऴि"],
"pl":["Polish","Polski"],
"ps":["Pashto","پښتو"],
"pt":["Portuguese","Português"],
"qu":["Quechua","Runa Simi"],
"rm":["Raeto Romance","Rumantsch"],
"rn":["Kirundi","Kirundi"],
"ro":["Romanian","Română"],
"ru":["Russian","Русский"],
"rw":["Rwandi","Kinyarwandi"],
"sa":["Sanskrit","संस्कृतम्"],
"sc":["Sardinian","Sardu"],
"sd":["Sindhi","सिनधि"],
"se":["Northern Sami","Sámegiella"],
"sg":["Sango","Sängö"],
"sh":["Serbo-Croatian","Srpskohrvatski / Српскохрватски"],
"si":["Sinhalese","සිංහල"],
"sk":["Slovak","Slovenčina"],
"sl":["Slovenian","Slovenščina"],
"sm":["Samoan","Gagana Samoa"],
"sn":["Shona","chiShona"],
"so":["Somalia","Soomaaliga"],
"sq":["Albanian","Shqip"],
"sr":["Serbian","Српски"],
"ss":["Swati","SiSwati"],
"st":["Southern Sotho","Sesotho"],
"su":["Sundanese","Basa Sunda"],
"sv":["Swedish","Svenska"],
"sw":["Swahili","Kiswahili"],
"ta":["Tamil","தமிழ்"],
"te":["Telugu","తెలుగు"],
"tg":["Tajik","Тоҷикӣ"],
"th":["Thai","ไทย / Phasa Thai"],
"ti":["Tigrinya","ትግርኛ"],
"tk":["Turkmen","Туркмен / تركمن"],
"tl":["Tagalog / Filipino","Tagalog"],
"tn":["Tswana","Setswana"],
"to":["Tonga","Lea Faka-Tonga"],
"tr":["Turkish","Türkçe"],
"ts":["Tsonga","Xitsonga"],
"tt":["Tatar","Tatarça"],
"tw":["Twi","Twi"],
"ty":["Tahitian","Reo Mā`ohi"],
"ug":["Uyghur","Uyƣurqə / ئۇيغۇرچە"],
"uk":["Ukrainian","Українська"],
"ur":["Urdu","اردو"],
"uz":["Uzbek","Ўзбек"],
"ve":["Venda","Tshivenḓa"],
"vi":["Vietnamese","Tiếng Việt"],
"vo":["Volapük","Volapük"],
"wa":["Walloon","Walon"],
"wo":["Wolof","Wollof"],
"xh":["Xhosa","isiXhosa"],
"yi":["Yiddish","ייִדיש"],
"yo":["Yoruba","Yorùbá"],
"za":["Zhuang","Cuengh / Tôô / 壮语"],
"zh":["Chinese","中文"],
"zu":["Zulu","isiZulu"]
},
"supported": [
"en-US",
"en-CA",
"fr-QC",
"ach-UG",
"af-ZA",
"ar-SA",
"bg-BG",
"ca-ES",
"cs-CZ",
"cy-GB",
"de-DE",
"de-AT",
"de-CH",
"da-DK",
"el-GR",
"en-GB",
"en-AU",
"en-HK",
"en-IE",
"en-NZ",
"en-PR",
"es-ES",
"es-MX",
"et-EE",
"fi-FI",
"fr-FR",
"fr-CH",
"fur-IT",
"ga-IE",
"gd-GB",
"he-IL",
"hr-HR",
"hu-HU",
"hy-AM",
"id-ID",
"it-CH",
"it-IT",
"ja-JP",
"ka-GE",
"ko-KR",
"ky-KG",
"lt-LT",
"nb-NO",
"nl-BE",
"nl-NL",
"pt-PT",
"pt-BR",
"pl-PL",
"ro-RO",
"ru-RU",
"sco-GB",
"sh-HR",
"sk-SK",
"sl-SI",
"sr-SP",
"sv-SE",
"th-TH",
"tr-TR",
"uk-UA",
"vi-VN",
"zh-CN",
"zh-HK",
"zh-SG",
"zh-TW"
]
"attribute": { "name": 0, "nativeName": 1 },
"rtl": {
"ar": 1,
"dv": 1,
"fa": 1,
"ha": 1,
"he": 1,
"ks": 1,
"ku": 1,
"ps": 1,
"ur": 1,
"yi": 1
},
"lang": {
"aa": ["Afar", "Afar"],
"ab": ["Abkhazian", "Аҧсуа"],
"af": ["Afrikaans", "Afrikaans"],
"ak": ["Akan", "Akana"],
"am": ["Amharic", "አማርኛ"],
"an": ["Aragonese", "Aragonés"],
"ar": ["Arabic", "العربية"],
"as": ["Assamese", "অসমীয়া"],
"av": ["Avar", "Авар"],
"ay": ["Aymara", "Aymar"],
"az": ["Azerbaijani", "Azərbaycanca / آذربايجان"],
"ba": ["Bashkir", "Башҡорт"],
"be": ["Belarusian", "Беларуская"],
"bg": ["Bulgarian", "Български"],
"bh": ["Bihari", "भोजपुरी"],
"bi": ["Bislama", "Bislama"],
"bm": ["Bambara", "Bamanankan"],
"bn": ["Bengali", "বাংলা"],
"bo": ["Tibetan", "བོད་ཡིག / Bod skad"],
"br": ["Breton", "Brezhoneg"],
"bs": ["Bosnian", "Bosanski"],
"ca": ["Catalan", "Català"],
"ce": ["Chechen", "Нохчийн"],
"ch": ["Chamorro", "Chamoru"],
"co": ["Corsican", "Corsu"],
"cr": ["Cree", "Nehiyaw"],
"cs": ["Czech", "Česky"],
"cu": ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"],
"cv": ["Chuvash", "Чăваш"],
"cy": ["Welsh", "Cymraeg"],
"da": ["Danish", "Dansk"],
"de": ["German", "Deutsch"],
"dv": ["Divehi", "ދިވެހިބަސް"],
"dz": ["Dzongkha", "ཇོང་ཁ"],
"ee": ["Ewe", "Ɛʋɛ"],
"el": ["Greek", "Ελληνικά"],
"en": ["English", "English"],
"eo": ["Esperanto", "Esperanto"],
"es": ["Spanish", "Español"],
"et": ["Estonian", "Eesti"],
"eu": ["Basque", "Euskara"],
"fa": ["Persian", "فارسی"],
"ff": ["Peul", "Fulfulde"],
"fi": ["Finnish", "Suomi"],
"fj": ["Fijian", "Na Vosa Vakaviti"],
"fo": ["Faroese", "Føroyskt"],
"fr": ["French", "Français"],
"fy": ["West Frisian", "Frysk"],
"ga": ["Irish", "Gaeilge"],
"gd": ["Scottish Gaelic", "Gàidhlig"],
"gl": ["Galician", "Galego"],
"gn": ["Guarani", "Avañe'ẽ"],
"gu": ["Gujarati", "ગુજરાતી"],
"gv": ["Manx", "Gaelg"],
"ha": ["Hausa", "هَوُسَ"],
"he": ["Hebrew", "עברית"],
"hi": ["Hindi", "हिन्दी"],
"ho": ["Hiri Motu", "Hiri Motu"],
"hr": ["Croatian", "Hrvatski"],
"ht": ["Haitian", "Krèyol ayisyen"],
"hu": ["Hungarian", "Magyar"],
"hy": ["Armenian", "Հայերեն"],
"hz": ["Herero", "Otsiherero"],
"ia": ["Interlingua", "Interlingua"],
"id": ["Indonesian", "Bahasa Indonesia"],
"ie": ["Interlingue", "Interlingue"],
"ig": ["Igbo", "Igbo"],
"ii": ["Sichuan Yi", "ꆇꉙ / 四川彝语"],
"ik": ["Inupiak", "Iñupiak"],
"io": ["Ido", "Ido"],
"is": ["Icelandic", "Íslenska"],
"it": ["Italian", "Italiano"],
"iu": ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"],
"ja": ["Japanese", "日本語"],
"jv": ["Javanese", "Basa Jawa"],
"ka": ["Georgian", "ქართული"],
"kg": ["Kongo", "KiKongo"],
"ki": ["Kikuyu", "Gĩkũyũ"],
"kj": ["Kuanyama", "Kuanyama"],
"kk": ["Kazakh", "Қазақша"],
"kl": ["Greenlandic", "Kalaallisut"],
"km": ["Cambodian", "ភាសាខ្មែរ"],
"kn": ["Kannada", "ಕನ್ನಡ"],
"ko": ["Korean", "한국어"],
"kr": ["Kanuri", "Kanuri"],
"ks": ["Kashmiri", "कश्मीरी / كشميري"],
"ku": ["Kurdish", "Kurdî / كوردی"],
"kv": ["Komi", "Коми"],
"kw": ["Cornish", "Kernewek"],
"ky": ["Kirghiz", "Kırgızca / Кыргызча"],
"la": ["Latin", "Latina"],
"lb": ["Luxembourgish", "Lëtzebuergesch"],
"lg": ["Ganda", "Luganda"],
"li": ["Limburgian", "Limburgs"],
"ln": ["Lingala", "Lingála"],
"lo": ["Laotian", "ລາວ / Pha xa lao"],
"lt": ["Lithuanian", "Lietuvių"],
"lv": ["Latvian", "Latviešu"],
"mg": ["Malagasy", "Malagasy"],
"mh": ["Marshallese", "Kajin Majel / Ebon"],
"mi": ["Maori", "Māori"],
"mk": ["Macedonian", "Македонски"],
"ml": ["Malayalam", "മലയാളം"],
"mn": ["Mongolian", "Монгол"],
"mo": ["Moldovan", "Moldovenească"],
"mr": ["Marathi", "मराठी"],
"ms": ["Malay", "Bahasa Melayu"],
"mt": ["Maltese", "bil-Malti"],
"my": ["Burmese", "Myanmasa"],
"na": ["Nauruan", "Dorerin Naoero"],
"nd": ["North Ndebele", "Sindebele"],
"ne": ["Nepali", "नेपाली"],
"ng": ["Ndonga", "Oshiwambo"],
"nl": ["Dutch", "Nederlands"],
"nn": ["Norwegian Nynorsk", "Norsk (nynorsk)"],
"no": ["Norwegian", "Norsk (bokmål / riksmål)"],
"nr": ["South Ndebele", "isiNdebele"],
"nv": ["Navajo", "Diné bizaad"],
"ny": ["Chichewa", "Chi-Chewa"],
"oc": ["Occitan", "Occitan"],
"oj": ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
"om": ["Oromo", "Oromoo"],
"or": ["Oriya", "ଓଡ଼ିଆ"],
"os": ["Ossetian / Ossetic", "Иронау"],
"pa": ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
"pi": ["Pali", "Pāli / पाऴि"],
"pl": ["Polish", "Polski"],
"ps": ["Pashto", "پښتو"],
"pt": ["Portuguese", "Português"],
"qu": ["Quechua", "Runa Simi"],
"rm": ["Raeto Romance", "Rumantsch"],
"rn": ["Kirundi", "Kirundi"],
"ro": ["Romanian", "Română"],
"ru": ["Russian", "Русский"],
"rw": ["Rwandi", "Kinyarwandi"],
"sa": ["Sanskrit", "संस्कृतम्"],
"sc": ["Sardinian", "Sardu"],
"sd": ["Sindhi", "सिनधि"],
"se": ["Northern Sami", "Sámegiella"],
"sg": ["Sango", "Sängö"],
"sh": ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"],
"si": ["Sinhalese", "සිංහල"],
"sk": ["Slovak", "Slovenčina"],
"sl": ["Slovenian", "Slovenščina"],
"sm": ["Samoan", "Gagana Samoa"],
"sn": ["Shona", "chiShona"],
"so": ["Somalia", "Soomaaliga"],
"sq": ["Albanian", "Shqip"],
"sr": ["Serbian", "Српски"],
"ss": ["Swati", "SiSwati"],
"st": ["Southern Sotho", "Sesotho"],
"su": ["Sundanese", "Basa Sunda"],
"sv": ["Swedish", "Svenska"],
"sw": ["Swahili", "Kiswahili"],
"ta": ["Tamil", "தமிழ்"],
"te": ["Telugu", "తెలుగు"],
"tg": ["Tajik", "Тоҷикӣ"],
"th": ["Thai", "ไทย / Phasa Thai"],
"ti": ["Tigrinya", "ትግርኛ"],
"tk": ["Turkmen", "Туркмен / تركمن"],
"tl": ["Tagalog / Filipino", "Tagalog"],
"tn": ["Tswana", "Setswana"],
"to": ["Tonga", "Lea Faka-Tonga"],
"tr": ["Turkish", "Türkçe"],
"ts": ["Tsonga", "Xitsonga"],
"tt": ["Tatar", "Tatarça"],
"tw": ["Twi", "Twi"],
"ty": ["Tahitian", "Reo Mā`ohi"],
"ug": ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"],
"uk": ["Ukrainian", "Українська"],
"ur": ["Urdu", "اردو"],
"uz": ["Uzbek", "Ўзбек"],
"ve": ["Venda", "Tshivenḓa"],
"vi": ["Vietnamese", "Tiếng Việt"],
"vo": ["Volapük", "Volapük"],
"wa": ["Walloon", "Walon"],
"wo": ["Wolof", "Wollof"],
"xh": ["Xhosa", "isiXhosa"],
"yi": ["Yiddish", "ייִדיש"],
"yo": ["Yoruba", "Yorùbá"],
"za": ["Zhuang", "Cuengh / Tôô / 壮语"],
"zh": ["Chinese", "中文"],
"zu": ["Zulu", "isiZulu"]
},
"supported": [
"en-US",
"en-CA",
"fr-QC",
"ach-UG",
"af-ZA",
"ar-SA",
"bg-BG",
"ca-ES",
"cs-CZ",
"cy-GB",
"de-DE",
"de-AT",
"de-CH",
"da-DK",
"el-GR",
"en-GB",
"en-AU",
"en-HK",
"en-IE",
"en-NZ",
"en-PR",
"es-ES",
"es-MX",
"et-EE",
"fi-FI",
"fr-FR",
"fr-CH",
"fur-IT",
"ga-IE",
"gd-GB",
"he-IL",
"hr-HR",
"hu-HU",
"hy-AM",
"id-ID",
"it-CH",
"it-IT",
"ja-JP",
"ka-GE",
"ko-KR",
"ky-KG",
"lt-LT",
"nb-NO",
"nl-BE",
"nl-NL",
"pt-PT",
"pt-BR",
"pl-PL",
"ro-RO",
"ru-RU",
"sco-GB",
"sh-HR",
"sk-SK",
"sl-SI",
"sr-SP",
"sv-SE",
"th-TH",
"tr-TR",
"uk-UA",
"vi-VN",
"zh-CN",
"zh-HK",
"zh-SG",
"zh-TW"
]
}

View file

@ -7,10 +7,10 @@ const countries = require('./data/countries.json')
const currenciesRec = require('./data/currencies.json')
const languageRec = require('./data/languages.json')
function massageCurrencies (currencies) {
function massageCurrencies(currencies) {
const convert = r => ({
code: r['Alphabetic Code'],
display: r['Currency']
display: r['Currency'],
})
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
const mapped = _.map(convert, currencies)
@ -37,7 +37,7 @@ const massageCryptos = cryptos => {
code: crypto['cryptoCode'],
display: crypto['display'],
codeDisplay: crypto['cryptoCodeDisplay'] ?? crypto['cryptoCode'],
isBeta: betaList.includes(crypto.cryptoCode)
isBeta: betaList.includes(crypto.cryptoCode),
})
return _.map(convert, cryptos)

View file

@ -2,7 +2,7 @@ const db = require('../db')
const cashInTx = require('../cash-in/cash-in-tx')
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
function transaction () {
function transaction() {
const sql = `SELECT DISTINCT * FROM (
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION
@ -27,7 +27,7 @@ function transaction () {
return db.any(sql)
}
function customer () {
function customer() {
const sql = `SELECT DISTINCT * FROM (
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION

View file

@ -7,7 +7,7 @@ const { AuthenticationError } = require('../errors')
function authDirectiveTransformer(schema, directiveName = 'auth') {
return mapSchema(schema, {
// For object types
[MapperKind.OBJECT_TYPE]: (objectType) => {
[MapperKind.OBJECT_TYPE]: objectType => {
const directive = getDirective(schema, objectType, directiveName)?.[0]
if (directive) {
const requiredAuthRole = directive.requires
@ -15,7 +15,7 @@ function authDirectiveTransformer(schema, directiveName = 'auth') {
}
return objectType
},
// For field definitions
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0]
@ -23,26 +23,30 @@ function authDirectiveTransformer(schema, directiveName = 'auth') {
const requiredAuthRole = directive.requires
fieldConfig._requiredAuthRole = requiredAuthRole
}
// Get the parent object type
const objectType = schema.getType(typeName)
// Apply auth check to the field's resolver
const { resolve = defaultFieldResolver } = fieldConfig
fieldConfig.resolve = function (root, args, context, info) {
const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
const requiredRoles =
fieldConfig._requiredAuthRole || objectType._requiredAuthRole
if (!requiredRoles)
return resolve.apply(this, [root, args, context, info])
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!')
throw new AuthenticationError(
'You do not have permission to access this resource!',
)
}
return resolve.apply(this, [root, args, context, info])
}
return fieldConfig
}
},
})
}

View file

@ -5,8 +5,8 @@ class AuthenticationError extends GraphQLError {
constructor() {
super('Authentication failed', {
extensions: {
code: 'UNAUTHENTICATED'
}
code: 'UNAUTHENTICATED',
},
})
}
}
@ -15,8 +15,8 @@ class InvalidCredentialsError extends GraphQLError {
constructor() {
super('Invalid credentials', {
extensions: {
code: 'INVALID_CREDENTIALS'
}
code: 'INVALID_CREDENTIALS',
},
})
}
}
@ -25,8 +25,8 @@ class UserAlreadyExistsError extends GraphQLError {
constructor() {
super('User already exists', {
extensions: {
code: 'USER_ALREADY_EXISTS'
}
code: 'USER_ALREADY_EXISTS',
},
})
}
}
@ -35,8 +35,8 @@ class InvalidTwoFactorError extends GraphQLError {
constructor() {
super('Invalid two-factor code', {
extensions: {
code: 'INVALID_TWO_FACTOR_CODE'
}
code: 'INVALID_TWO_FACTOR_CODE',
},
})
}
}
@ -45,8 +45,8 @@ class InvalidUrlError extends GraphQLError {
constructor() {
super('Invalid URL token', {
extensions: {
code: 'INVALID_URL_TOKEN'
}
code: 'INVALID_URL_TOKEN',
},
})
}
}
@ -55,8 +55,8 @@ class UserInputError extends GraphQLError {
constructor() {
super('User input error', {
extensions: {
code: ApolloServerErrorCode.BAD_USER_INPUT
}
code: ApolloServerErrorCode.BAD_USER_INPUT,
},
})
}
}
@ -67,5 +67,5 @@ module.exports = {
UserAlreadyExistsError,
InvalidTwoFactorError,
InvalidUrlError,
UserInputError
}
UserInputError,
}

View file

@ -12,60 +12,70 @@ const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
return users
.getUserById(options.userId)
.then(user => {
return Promise.all([
credentials.getHardwareCredentialsByUserId(user.id),
user,
])
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
.then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
allowCredentials: devices.map(dev => ({
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
userVerification: 'discouraged',
rpID: options.domain
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false,
},
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
attestation: {
challenge: opts.challenge,
},
}
return opts
})
})
}
const generateAssertionOptions = (session, options) => {
return userManagement
.authenticateUser(options.username, options.password)
.then(user => {
return credentials
.getHardwareCredentialsByUserId(user.id)
.then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
userVerification: 'discouraged',
rpID: options.domain,
})
session.webauthn = {
assertion: {
challenge: opts.challenge,
},
}
return opts
})
})
}
const validateAttestation = (session, options) => {
@ -78,98 +88,112 @@ const validateAttestation = (session, options) => {
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
expectedRPID: options.domain,
}),
]).then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
const { counter, credentialPublicKey, credentialID } = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
return credentials
.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(
device => device.data.credentialID === credentialID,
)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID
}
credentials.createHardwareCredential(user.id, newDevice)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID,
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return userManagement
.authenticateUser(options.username, options.password)
.then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
return credentials
.getHardwareCredentialsByUserId(user.id)
.then(devices => {
const dbAuthenticator = _.find(dev => {
return (
Buffer.from(dev.data.credentialID).compare(
base64url.toBuffer(options.assertionResponse.rawId),
) === 0
)
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
if (!dbAuthenticator.data) {
throw new Error(
`Could not find authenticator matching ${options.assertionResponse.id}`,
)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
credentialPublicKey: Buffer.from(
dbAuthenticator.data.credentialPublicKey,
),
})
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator,
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
const { verified, assertionInfo } = verification
if (!verified) {
session.webauthn = null
return false
}
if (!verified) {
session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials
.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = {
id: user.id,
username: user.username,
role: user.role,
}
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
session.webauthn = null
return verified
})
})
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
validateAssertion,
}

View file

@ -11,35 +11,41 @@ const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
return users
.getUserById(options.userId)
.then(user => {
return Promise.all([
credentials.getHardwareCredentialsByUserId(user.id),
user,
])
})
.then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false,
},
})
session.webauthn = {
attestation: {
challenge: opts.challenge
session.webauthn = {
attestation: {
challenge: opts.challenge,
},
}
}
return opts
})
return opts
})
}
const generateAssertionOptions = (session, options) => {
@ -50,16 +56,16 @@ const generateAssertionOptions = (session, options) => {
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
userVerification: 'discouraged',
rpID: options.domain
rpID: options.domain,
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
challenge: opts.challenge,
},
}
return opts
@ -77,40 +83,38 @@ const validateAttestation = (session, options) => {
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
expectedRPID: options.domain,
}),
]).then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
const { counter, credentialPublicKey, credentialID } = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
return credentials
.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(
device => device.data.credentialID === credentialID,
)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID
}
credentials.createHardwareCredential(user.id, newDevice)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID,
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
@ -119,17 +123,24 @@ const validateAssertion = (session, options) => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
return (
Buffer.from(dev.data.credentialID).compare(
base64url.toBuffer(options.assertionResponse.rawId),
) === 0
)
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
throw new Error(
`Could not find authenticator matching ${options.assertionResponse.id}`,
)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
credentialPublicKey: Buffer.from(
dbAuthenticator.data.credentialPublicKey,
),
})
let verification
try {
@ -138,7 +149,7 @@ const validateAssertion = (session, options) => {
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
authenticator: convertedAuthenticator,
})
} catch (err) {
console.error(err)
@ -148,20 +159,22 @@ const validateAssertion = (session, options) => {
const { verified, assertionInfo } = verification
if (!verified) {
context.req.session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
return credentials.updateHardwareCredential(dbAuthenticator).then(() => {
const finalUser = {
id: user.id,
username: user.username,
role: user.role,
}
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
session.webauthn = null
return verified
})
})
})
}
@ -170,5 +183,5 @@ module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
validateAssertion,
}

View file

@ -22,19 +22,19 @@ const generateAttestationOptions = (session, options) => {
excludeCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
userVerification: 'discouraged',
requireResidentKey: false
}
requireResidentKey: false,
},
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
challenge: opts.challenge,
},
}
return opts
@ -48,16 +48,16 @@ const generateAssertionOptions = (session, options) => {
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
transports: ['usb', 'ble', 'nfc', 'internal'],
})),
userVerification: 'discouraged',
rpID: options.domain
rpID: options.domain,
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
challenge: opts.challenge,
},
}
return opts
})
@ -73,50 +73,52 @@ const validateAttestation = (session, options) => {
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
expectedRPID: options.domain,
}),
]).then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return verified
}
const {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject,
} = attestationInfo
return credentials
.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(
device => device.data.credentialID === credentialID,
)
if (!existingDevice) {
const newDevice = {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject,
}
credentials.createHardwareCredential(user.id, newDevice)
}
if (!(verified || attestationInfo)) {
session.webauthn = null
return verified
}
const {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
if (!existingDevice) {
const newDevice = {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
})
})
}
const validateAssertion = (session, options) => {
@ -124,17 +126,24 @@ const validateAssertion = (session, options) => {
return credentials.getHardwareCredentials().then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
return (
Buffer.from(dev.data.credentialID).compare(
base64url.toBuffer(options.assertionResponse.rawId),
) === 0
)
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
throw new Error(
`Could not find authenticator matching ${options.assertionResponse.id}`,
)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
credentialPublicKey: Buffer.from(
dbAuthenticator.data.credentialPublicKey,
),
})
let verification
try {
@ -143,7 +152,7 @@ const validateAssertion = (session, options) => {
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
authenticator: convertedAuthenticator,
})
} catch (err) {
console.error(err)
@ -160,16 +169,19 @@ const validateAssertion = (session, options) => {
dbAuthenticator.data.counter = assertionInfo.newCounter
return Promise.all([
credentials.updateHardwareCredential(dbAuthenticator),
users.getUserById(dbAuthenticator.user_id)
])
.then(([_, user]) => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
users.getUserById(dbAuthenticator.user_id),
]).then(([, user]) => {
const finalUser = {
id: user.id,
username: user.username,
role: user.role,
}
session.user = finalUser
session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
}
@ -177,5 +189,5 @@ module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
validateAssertion,
}

View file

@ -5,7 +5,7 @@ const FIDOUsernameless = require('./FIDOUsernamelessStrategy')
const STRATEGIES = {
FIDO2FA,
FIDOPasswordless,
FIDOUsernameless
FIDOUsernameless,
}
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
@ -13,5 +13,5 @@ const CHOSEN_STRATEGY = 'FIDO2FA'
module.exports = {
CHOSEN_STRATEGY,
strategy: STRATEGIES[CHOSEN_STRATEGY]
strategy: STRATEGIES[CHOSEN_STRATEGY],
}

View file

@ -14,11 +14,16 @@ const credentials = require('../../../hardware-credentials')
const REMEMBER_ME_AGE = 90 * T.day
const authenticateUser = (username, password) => {
return users.getUserByUsername(username)
return users
.getUserByUsername(username)
.then(user => {
const hashedPassword = user.password
if (!hashedPassword || !user.enabled) throw new authErrors.InvalidCredentialsError()
return Promise.all([argon2.verify(hashedPassword, password), hashedPassword])
if (!hashedPassword || !user.enabled)
throw new authErrors.InvalidCredentialsError()
return Promise.all([
argon2.verify(hashedPassword, password),
hashedPassword,
])
})
.then(([isMatch, hashedPassword]) => {
if (!isMatch) throw new authErrors.InvalidCredentialsError()
@ -32,7 +37,9 @@ const authenticateUser = (username, password) => {
const destroySessionIfSameUser = (context, user) => {
const sessionUser = getUserFromCookie(context)
if (sessionUser && user.id === sessionUser.id) { context.req.session.destroy() }
if (sessionUser && user.id === sessionUser.id) {
context.req.session.destroy()
}
}
const destroySessionIfBeingUsed = (sessID, context) => {
@ -56,15 +63,13 @@ const initializeSession = (context, user, rememberMe) => {
}
const executeProtectedAction = (code, id, context, action) => {
return users.getUserById(id)
.then(user => {
if (user.role !== 'superuser') {
return action()
}
return users.getUserById(id).then(user => {
if (user.role !== 'superuser') {
return action()
}
return confirm2FA(code, context)
.then(() => action())
})
return confirm2FA(code, context).then(() => action())
})
}
const getUserData = context => {
@ -79,10 +84,18 @@ const get2FASecret = (username, password) => {
return authenticateUser(username, password)
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
return Promise.all([users.saveTemp2FASecret(user.id, secret), secret, otpauth])
const otpauth = otplib.authenticator.keyuri(
user.username,
constants.AUTHENTICATOR_ISSUER_ENTITY,
secret,
)
return Promise.all([
users.saveTemp2FASecret(user.id, secret),
secret,
otpauth,
])
})
.then(([_, secret, otpauth]) => {
.then(([, secret, otpauth]) => {
return { secret, otpauth }
})
}
@ -103,35 +116,43 @@ const confirm2FA = (token, context) => {
const validateRegisterLink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateUserRegistrationToken(token)
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { username: r.username, role: r.role }
})
return users.validateUserRegistrationToken(token).then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { username: r.username, role: r.role }
})
}
const validateResetPasswordLink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateAuthToken(token, 'reset_password')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { id: r.userID }
})
return users.validateAuthToken(token, 'reset_password').then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { id: r.userID }
})
}
const validateReset2FALink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateAuthToken(token, 'reset_twofa')
return users
.validateAuthToken(token, 'reset_twofa')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return users.getUserById(r.userID)
})
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
return Promise.all([users.saveTemp2FASecret(user.id, secret), user, secret, otpauth])
const otpauth = otplib.authenticator.keyuri(
user.username,
constants.AUTHENTICATOR_ISSUER_ENTITY,
secret,
)
return Promise.all([
users.saveTemp2FASecret(user.id, secret),
user,
secret,
otpauth,
])
})
.then(([_, user, secret, otpauth]) => {
.then(([, user, secret, otpauth]) => {
return { user_id: user.id, secret, otpauth }
})
}
@ -144,7 +165,10 @@ const deleteSession = (sessionID, context) => {
const login = (username, password) => {
return authenticateUser(username, password)
.then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user.twofa_code])
return Promise.all([
credentials.getHardwareCredentialsByUserId(user.id),
user.twofa_code,
])
})
.then(([devices, twoFASecret]) => {
if (!_.isEmpty(devices)) return 'FIDO'
@ -153,21 +177,32 @@ const login = (username, password) => {
}
const input2FA = (username, password, rememberMe, code, context) => {
return authenticateUser(username, password)
.then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
initializeSession(context, user, rememberMe)
return true
return authenticateUser(username, password).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({
token: code,
secret: secret,
})
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
initializeSession(context, user, rememberMe)
return true
})
}
const setup2FA = (username, password, rememberMe, codeConfirmation, context) => {
const setup2FA = (
username,
password,
rememberMe,
codeConfirmation,
context,
) => {
return authenticateUser(username, password)
.then(user => {
const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: user.temp_twofa_code })
const isCodeValid = otplib.authenticator.verify({
token: codeConfirmation,
secret: user.temp_twofa_code,
})
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
initializeSession(context, user, rememberMe)
@ -202,24 +237,23 @@ const createReset2FAToken = (code, userID, context) => {
}
const createRegisterToken = (username, role) => {
return users.getUserByUsername(username)
.then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.getUserByUsername(username).then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.createUserRegistrationToken(username, role)
})
return users.createUserRegistrationToken(username, role)
})
}
const register = (token, username, password, role) => {
return users.getUserByUsername(username)
.then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.register(token, username, password, role).then(() => true)
})
return users.getUserByUsername(username).then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.register(token, username, password, role).then(() => true)
})
}
const resetPassword = (token, userID, newPassword, context) => {
return users.getUserById(userID)
return users
.getUserById(userID)
.then(user => {
destroySessionIfSameUser(context, user)
return users.updatePassword(token, user.id, newPassword)
@ -228,9 +262,13 @@ const resetPassword = (token, userID, newPassword, context) => {
}
const reset2FA = (token, userID, code, context) => {
return users.getUserById(userID)
return users
.getUserById(userID)
.then(user => {
const isCodeValid = otplib.authenticator.verify({ token: code, secret: user.temp_twofa_code })
const isCodeValid = otplib.authenticator.verify({
token: code,
secret: user.temp_twofa_code,
})
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
destroySessionIfSameUser(context, user)
@ -240,7 +278,10 @@ const reset2FA = (token, userID, code, context) => {
}
const getToken = context => {
if (_.isNil(context.req.cookies['lamassu_sid']) || _.isNil(context.req.session.user.id))
if (
_.isNil(context.req.cookies['lamassu_sid']) ||
_.isNil(context.req.session.user.id)
)
throw new authErrors.AuthenticationError('Authentication failed')
return context.req.session.user.id
@ -267,5 +308,5 @@ module.exports = {
register,
resetPassword,
reset2FA,
getToken
getToken,
}

View file

@ -2,8 +2,8 @@ const bills = require('../../services/bills')
const resolvers = {
Query: {
bills: (...[, { filters }]) => bills.getBills(filters)
}
bills: (...[, { filters }]) => bills.getBills(filters),
},
}
module.exports = resolvers

View file

@ -3,7 +3,7 @@ const blacklist = require('../../../blacklist')
const resolvers = {
Query: {
blacklist: () => blacklist.getBlacklist(),
blacklistMessages: () => blacklist.getMessages()
blacklistMessages: () => blacklist.getMessages(),
},
Mutation: {
deleteBlacklistRow: (...[, { address }]) =>
@ -11,8 +11,8 @@ const resolvers = {
insertBlacklistRow: (...[, { address }]) =>
blacklist.insertIntoBlacklist(address),
editBlacklistMessage: (...[, { id, content }]) =>
blacklist.editBlacklistMessage(id, content)
}
blacklist.editBlacklistMessage(id, content),
},
}
module.exports = resolvers

View file

@ -5,13 +5,21 @@ const logDateFormat = require('../../../logs').logDateFormat
const resolvers = {
Query: {
cashboxBatches: () => cashbox.getBatches(),
cashboxBatchesCsv: (...[, { from, until, timezone }]) => cashbox.getBatches(from, until)
.then(data => parseAsync(logDateFormat(timezone, cashbox.logFormatter(data), ['created'])))
cashboxBatchesCsv: (...[, { from, until, timezone }]) =>
cashbox
.getBatches(from, until)
.then(data =>
parseAsync(
logDateFormat(timezone, cashbox.logFormatter(data), ['created']),
),
),
},
Mutation: {
createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount),
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
}
createBatch: (...[, { deviceId, cashboxCount }]) =>
cashbox.createCashboxBatch(deviceId, cashboxCount),
editBatch: (...[, { id, performedBy }]) =>
cashbox.editBatchById(id, performedBy),
},
}
module.exports = resolvers

View file

@ -1,11 +1,15 @@
const { accounts: accountsConfig, countries, languages } = require('../../config')
const {
accounts: accountsConfig,
countries,
languages,
} = require('../../config')
const resolver = {
Query: {
countries: () => countries,
languages: () => languages,
accountsConfig: () => accountsConfig
}
accountsConfig: () => accountsConfig,
},
}
module.exports = resolver

View file

@ -3,8 +3,8 @@ const { coins, currencies } = require('../../config')
const resolver = {
Query: {
currencies: () => currencies,
cryptoCurrencies: () => coins
}
cryptoCurrencies: () => coins,
},
}
module.exports = resolver

View file

@ -2,32 +2,55 @@ const authentication = require('../modules/userManagement')
const queries = require('../../services/customInfoRequests')
const DataLoader = require('dataloader')
const customerCustomInfoRequestsLoader = new DataLoader(ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), { cache: false })
const customerCustomInfoRequestsLoader = new DataLoader(
ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids),
{ cache: false },
)
const customInfoRequestLoader = new DataLoader(ids => queries.batchGetCustomInfoRequest(ids), { cache: false })
const customInfoRequestLoader = new DataLoader(
ids => queries.batchGetCustomInfoRequest(ids),
{ cache: false },
)
const resolvers = {
Customer: {
customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id)
customInfoRequests: parent =>
customerCustomInfoRequestsLoader.load(parent.id),
},
CustomRequestData: {
customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId)
customInfoRequest: parent =>
customInfoRequestLoader.load(parent.infoRequestId),
},
Query: {
customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled),
customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId),
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId)
customInfoRequests: (...[, { onlyEnabled }]) =>
queries.getCustomInfoRequests(onlyEnabled),
customerCustomInfoRequests: (...[, { customerId }]) =>
queries.getAllCustomInfoRequestsForCustomer(customerId),
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) =>
queries.getCustomInfoRequestForCustomer(customerId, infoRequestId),
},
Mutation: {
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => {
insertCustomInfoRequest: (...[, { customRequest }]) =>
queries.addCustomInfoRequest(customRequest),
removeCustomInfoRequest: (...[, { id }]) =>
queries.removeCustomInfoRequest(id),
editCustomInfoRequest: (...[, { id, customRequest }]) =>
queries.editCustomInfoRequest(id, customRequest),
setAuthorizedCustomRequest: (
...[, { customerId, infoRequestId, override }, context]
) => {
const token = authentication.getToken(context)
return queries.setAuthorizedCustomRequest(customerId, infoRequestId, override, token)
return queries.setAuthorizedCustomRequest(
customerId,
infoRequestId,
override,
token,
)
},
setCustomerCustomInfoRequest: (...[, { customerId, infoRequestId, data }]) => queries.setCustomerData(customerId, infoRequestId, data)
}
setCustomerCustomInfoRequest: (
...[, { customerId, infoRequestId, data }]
) => queries.setCustomerData(customerId, infoRequestId, data),
},
}
module.exports = resolvers

View file

@ -6,41 +6,56 @@ const customerNotes = require('../../../customer-notes')
const machineLoader = require('../../../machine-loader')
const addLastUsedMachineName = customer =>
(customer.lastUsedMachine ? machineLoader.getMachineName(customer.lastUsedMachine) : Promise.resolve(null))
.then(lastUsedMachineName => Object.assign(customer, { lastUsedMachineName }))
(customer.lastUsedMachine
? machineLoader.getMachineName(customer.lastUsedMachine)
: Promise.resolve(null)
).then(lastUsedMachineName =>
Object.assign(customer, { lastUsedMachineName }),
)
const resolvers = {
Customer: {
isAnonymous: parent => (parent.customerId === anonymous.uuid)
isAnonymous: parent => parent.customerId === anonymous.uuid,
},
Query: {
customers: (...[, { phone, email, name, address, id }]) => customers.getCustomersList(phone, name, address, id, email),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId).then(addLastUsedMachineName),
customerFilters: () => filters.customer()
customers: (...[, { phone, email, name, address, id }]) =>
customers.getCustomersList(phone, name, address, id, email),
customer: (...[, { customerId }]) =>
customers.getCustomerById(customerId).then(addLastUsedMachineName),
customerFilters: () => filters.customer(),
},
Mutation: {
setCustomer: (root, { customerId, customerInput }, context, info) => {
setCustomer: (root, { customerId, customerInput }, context) => {
const token = authentication.getToken(context)
if (customerId === anonymous.uuid) return customers.getCustomerById(customerId)
if (customerId === anonymous.uuid)
return customers.getCustomerById(customerId)
return customers.updateCustomer(customerId, customerInput, token)
},
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value),
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
addCustomField: (...[, { customerId, label, value }]) =>
customers.addCustomField(customerId, label, value),
saveCustomField: (...[, { customerId, fieldId, value }]) =>
customers.saveCustomField(customerId, fieldId, value),
removeCustomField: (...[, [{ customerId, fieldId }]]) =>
customers.removeCustomField(customerId, fieldId),
editCustomer: async (root, { customerId, customerEdit }, context) => {
const token = authentication.getToken(context)
const editedData = await customerEdit
return customers.edit(customerId, editedData, token)
},
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
replacePhoto: async (
root,
{ customerId, photoType, newPhoto },
context,
) => {
const token = authentication.getToken(context)
const { file } = newPhoto
const photo = await file
if (!photo) return customers.getCustomerById(customerId)
return customers.updateEditedPhoto(customerId, photo, photoType)
return customers
.updateEditedPhoto(customerId, photo, photoType)
.then(newPatch => customers.edit(customerId, newPatch, token))
},
deleteEditedData: (root, { customerId, customerEdit }) => {
deleteEditedData: (root, { customerId }) => {
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
return customers.getCustomerById(customerId)
},
@ -55,12 +70,13 @@ const resolvers = {
deleteCustomerNote: (...[, { noteId }]) => {
return customerNotes.deleteCustomerNote(noteId)
},
createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }),
createCustomer: (...[, { phoneNumber }]) =>
customers.add({ phone: phoneNumber }),
enableTestCustomer: (...[, { customerId }]) =>
customers.enableTestCustomer(customerId),
disableTestCustomer: (...[, { customerId }]) =>
customers.disableTestCustomer(customerId)
}
customers.disableTestCustomer(customerId),
},
}
module.exports = resolvers

View file

@ -2,8 +2,8 @@ const funding = require('../../services/funding')
const resolvers = {
Query: {
funding: () => funding.getFunding()
}
funding: () => funding.getFunding(),
},
}
module.exports = resolvers

View file

@ -47,7 +47,7 @@ const resolvers = [
status,
transaction,
user,
version
version,
]
module.exports = mergeResolvers(resolvers)

View file

@ -1,5 +1,4 @@
const { parseAsync } = require('json2csv')
const _ = require('lodash/fp')
const logs = require('../../../logs')
const serverLogs = require('../../services/server-logs')
@ -8,15 +7,23 @@ const resolvers = {
Query: {
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
machineLogsCsv: (...[, { deviceId, from, until, limit, offset, timezone }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset)
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp']))),
machineLogsCsv: (
...[, { deviceId, from, until, limit, offset, timezone }]
) =>
logs
.simpleGetMachineLogs(deviceId, from, until, limit, offset)
.then(res =>
parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])),
),
serverLogs: (...[, { from, until, limit, offset }]) =>
serverLogs.getServerLogs(from, until, limit, offset),
serverLogsCsv: (...[, { from, until, limit, offset, timezone }]) =>
serverLogs.getServerLogs(from, until, limit, offset)
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])))
}
serverLogs
.getServerLogs(from, until, limit, offset)
.then(res =>
parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])),
),
},
}
module.exports = resolvers

View file

@ -3,24 +3,30 @@ const DataLoader = require('dataloader')
const loyalty = require('../../../loyalty')
const { getSlimCustomerByIdBatch } = require('../../../customers')
const customerLoader = new DataLoader(ids => {
return getSlimCustomerByIdBatch(ids)
}, { cache: false })
const customerLoader = new DataLoader(
ids => {
return getSlimCustomerByIdBatch(ids)
},
{ cache: false },
)
const resolvers = {
IndividualDiscount: {
customer: parent => customerLoader.load(parent.customerId)
customer: parent => customerLoader.load(parent.customerId),
},
Query: {
promoCodes: () => loyalty.getAvailablePromoCodes(),
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts()
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts(),
},
Mutation: {
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
createPromoCode: (...[, { code, discount }]) =>
loyalty.createPromoCode(code, discount),
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
createIndividualDiscount: (...[, { customerId, discount }]) => loyalty.createIndividualDiscount(customerId, discount),
deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId)
}
createIndividualDiscount: (...[, { customerId, discount }]) =>
loyalty.createIndividualDiscount(customerId, discount),
deleteIndividualDiscount: (...[, { discountId }]) =>
loyalty.deleteIndividualDiscount(discountId),
},
}
module.exports = resolvers

View file

@ -3,25 +3,29 @@ const DataLoader = require('dataloader')
const { machineAction } = require('../../services/machines')
const machineLoader = require('../../../machine-loader')
const machineEventsByIdBatch = require('../../../postgresql_interface').machineEventsByIdBatch
const machineEventsByIdBatch =
require('../../../postgresql_interface').machineEventsByIdBatch
const machineEventsLoader = new DataLoader(ids => {
return machineEventsByIdBatch(ids)
}, { cache: false })
const machineEventsLoader = new DataLoader(
ids => {
return machineEventsByIdBatch(ids)
},
{ cache: false },
)
const resolvers = {
Machine: {
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
latestEvent: parent => machineEventsLoader.load(parent.deviceId),
},
Query: {
machines: () => machineLoader.getMachineNames(),
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
unpairedMachines: () => machineLoader.getUnpairedMachines()
unpairedMachines: () => machineLoader.getUnpairedMachines(),
},
Mutation: {
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
machineAction({ deviceId, action, cashUnits, newName }, context)
}
machineAction({ deviceId, action, cashUnits, newName }, context),
},
}
module.exports = resolvers

View file

@ -2,8 +2,8 @@ const exchange = require('../../../exchange')
const resolvers = {
Query: {
getMarkets: () => exchange.getMarkets()
}
getMarkets: () => exchange.getMarkets(),
},
}
module.exports = resolvers

View file

@ -4,12 +4,13 @@ const resolvers = {
Query: {
notifications: () => notifierQueries.getNotifications(),
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications(),
alerts: () => notifierQueries.getAlerts()
alerts: () => notifierQueries.getAlerts(),
},
Mutation: {
toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read),
clearAllNotifications: () => notifierQueries.markAllAsRead()
}
toggleClearNotification: (...[, { id, read }]) =>
notifierQueries.setRead(id, read),
clearAllNotifications: () => notifierQueries.markAllAsRead(),
},
}
module.exports = resolvers

View file

@ -2,8 +2,8 @@ const pairing = require('../../services/pairing')
const resolvers = {
Mutation: {
createPairingTotem: (...[, { name }]) => pairing.totem(name)
}
createPairingTotem: (...[, { name }]) => pairing.totem(name),
},
}
module.exports = resolvers

View file

@ -10,12 +10,12 @@ const resolvers = {
return pi.getRawRates().then(r => {
return {
withCommissions: pi.buildRates(r),
withoutCommissions: pi.buildRatesNoCommission(r)
withoutCommissions: pi.buildRatesNoCommission(r),
}
})
}),
fiatRates: () => forex.getFiatRates()
}
fiatRates: () => forex.getFiatRates(),
},
}
module.exports = resolvers

View file

@ -6,8 +6,8 @@ const resolvers = {
checkAgainstSanctions: (...[, { customerId }, context]) => {
const token = authentication.getToken(context)
return sanctions.checkByUser(customerId, token)
}
}
},
},
}
module.exports = resolvers

View file

@ -1,9 +1,13 @@
const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars')
const {
DateTimeISOResolver,
JSONResolver,
JSONObjectResolver,
} = require('graphql-scalars')
const resolvers = {
JSON: JSONResolver,
JSONObject: JSONObjectResolver,
DateTimeISO: DateTimeISOResolver
DateTimeISO: DateTimeISOResolver,
}
module.exports = resolvers

View file

@ -3,12 +3,13 @@ const settingsLoader = require('../../../new-settings-loader')
const resolvers = {
Query: {
accounts: () => settingsLoader.showAccounts(),
config: () => settingsLoader.loadLatestConfigOrNone()
config: () => settingsLoader.loadLatestConfigOrNone(),
},
Mutation: {
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
saveAccounts: (...[, { accounts }]) =>
settingsLoader.saveAccounts(accounts),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config),
}
},
}
module.exports = resolvers

View file

@ -2,13 +2,14 @@ const smsNotices = require('../../../sms-notices')
const resolvers = {
Query: {
SMSNotices: () => smsNotices.getSMSNotices()
SMSNotices: () => smsNotices.getSMSNotices(),
},
Mutation: {
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
editSMSNotice: (...[, { id, event, message }]) =>
smsNotices.editSMSNotice(id, event, message),
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
}
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id),
},
}
module.exports = resolvers

View file

@ -2,8 +2,8 @@ const supervisor = require('../../services/supervisor')
const resolvers = {
Query: {
uptime: () => supervisor.getAllProcessInfo()
}
uptime: () => supervisor.getAllProcessInfo(),
},
}
module.exports = resolvers

View file

@ -1,6 +1,5 @@
const DataLoader = require('dataloader')
const { parseAsync } = require('json2csv')
const _ = require('lodash/fp')
const filters = require('../../filters')
const cashOutTx = require('../../../cash-out/cash-out-tx')
@ -9,35 +8,124 @@ const transactions = require('../../services/transactions')
const anonymous = require('../../../constants').anonymousCustomer
const logDateFormat = require('../../../logs').logDateFormat
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids), { cache: false })
const transactionsLoader = new DataLoader(
ids => transactions.getCustomerTransactionsBatch(ids),
{ cache: false },
)
const resolvers = {
Customer: {
transactions: parent => transactionsLoader.load(parent.id)
transactions: parent => transactionsLoader.load(parent.id),
},
Transaction: {
isAnonymous: parent => (parent.customerId === anonymous.uuid)
isAnonymous: parent => parent.customerId === anonymous.uuid,
},
Query: {
transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
transactions: (
...[
,
{
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
excludeTestingCustomers,
},
]
) =>
transactions.batch(
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
excludeTestingCustomers,
),
transactionsCsv: (
...[
,
{
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
timezone,
excludeTestingCustomers,
simplified,
},
]
) =>
transactions
.batch(
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
excludeTestingCustomers,
simplified,
)
.then(data =>
parseAsync(
logDateFormat(timezone, data, [
'created',
'sendTime',
'publishedAt',
]),
),
),
transactionCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTx(id, txClass).then(data =>
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt']))
),
transactions
.getTx(id, txClass)
.then(data =>
parseAsync(
logDateFormat(
timezone,
[data],
['created', 'sendTime', 'publishedAt'],
),
),
),
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTxAssociatedData(id, txClass).then(data =>
parseAsync(logDateFormat(timezone, data, ['created']))
),
transactionFilters: () => filters.transaction()
transactions
.getTxAssociatedData(id, txClass)
.then(data => parseAsync(logDateFormat(timezone, data, ['created']))),
transactionFilters: () => filters.transaction(),
},
Mutation: {
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id),
cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id)
}
cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id),
},
}
module.exports = resolvers

View file

@ -19,7 +19,11 @@ const getAttestationQueryOptions = variables => {
const getAssertionQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password, domain: variables.domain }
return {
username: variables.username,
password: variables.password,
domain: variables.domain,
}
case 'FIDOPasswordless':
return { username: variables.username, domain: variables.domain }
case 'FIDOUsernameless':
@ -32,11 +36,23 @@ const getAssertionQueryOptions = variables => {
const getAttestationMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
return {
userId: variables.userID,
attestationResponse: variables.attestationResponse,
domain: variables.domain,
}
case 'FIDOPasswordless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
return {
userId: variables.userID,
attestationResponse: variables.attestationResponse,
domain: variables.domain,
}
case 'FIDOUsernameless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
return {
userId: variables.userID,
attestationResponse: variables.attestationResponse,
domain: variables.domain,
}
default:
return {}
}
@ -45,11 +61,25 @@ const getAttestationMutationOptions = variables => {
const getAssertionMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
return {
username: variables.username,
password: variables.password,
rememberMe: variables.rememberMe,
assertionResponse: variables.assertionResponse,
domain: variables.domain,
}
case 'FIDOPasswordless':
return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
return {
username: variables.username,
rememberMe: variables.rememberMe,
assertionResponse: variables.assertionResponse,
domain: variables.domain,
}
case 'FIDOUsernameless':
return { assertionResponse: variables.assertionResponse, domain: variables.domain }
return {
assertionResponse: variables.assertionResponse,
domain: variables.domain,
}
default:
return {}
}
@ -59,34 +89,82 @@ const resolver = {
Query: {
users: () => users.getUsers(),
sessions: () => sessionManager.getSessions(),
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
userData: (...[, {}, context]) => userManagement.getUserData(context),
get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password),
confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => userManagement.validateReset2FALink(token),
generateAttestationOptions: (...[, variables, context]) => authentication.strategy.generateAttestationOptions(context.req.session, getAttestationQueryOptions(variables)),
generateAssertionOptions: (...[, variables, context]) => authentication.strategy.generateAssertionOptions(context.req.session, getAssertionQueryOptions(variables))
userSessions: (...[, { username }]) =>
sessionManager.getSessionsByUsername(username),
userData: (...[, , context]) => userManagement.getUserData(context),
get2FASecret: (...[, { username, password }]) =>
userManagement.get2FASecret(username, password),
confirm2FA: (...[, { code }, context]) =>
userManagement.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) =>
userManagement.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) =>
userManagement.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) =>
userManagement.validateReset2FALink(token),
generateAttestationOptions: (...[, variables, context]) =>
authentication.strategy.generateAttestationOptions(
context.req.session,
getAttestationQueryOptions(variables),
),
generateAssertionOptions: (...[, variables, context]) =>
authentication.strategy.generateAssertionOptions(
context.req.session,
getAssertionQueryOptions(variables),
),
},
Mutation: {
enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) => userManagement.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code }, context]) => userManagement.reset2FA(token, userID, code, context),
validateAttestation: (...[, variables, context]) => authentication.strategy.validateAttestation(context.req.session, getAttestationMutationOptions(variables)),
validateAssertion: (...[, variables, context]) => authentication.strategy.validateAssertion(context.req.session, getAssertionMutationOptions(variables))
}
enableUser: (...[, { confirmationCode, id }, context]) =>
userManagement.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) =>
userManagement.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) =>
userManagement.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) =>
sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) =>
userManagement.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) =>
userManagement.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) =>
userManagement.input2FA(username, password, rememberMe, code, context),
setup2FA: (
...[, { username, password, rememberMe, codeConfirmation }, context]
) =>
userManagement.setup2FA(
username,
password,
rememberMe,
codeConfirmation,
context,
),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) =>
userManagement.createResetPasswordToken(
confirmationCode,
userID,
context,
),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) =>
userManagement.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) =>
userManagement.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) =>
userManagement.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) =>
userManagement.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code }, context]) =>
userManagement.reset2FA(token, userID, code, context),
validateAttestation: (...[, variables, context]) =>
authentication.strategy.validateAttestation(
context.req.session,
getAttestationMutationOptions(variables),
),
validateAssertion: (...[, variables, context]) =>
authentication.strategy.validateAssertion(
context.req.session,
getAssertionMutationOptions(variables),
),
},
}
module.exports = resolver

View file

@ -2,8 +2,8 @@ const serverVersion = require('../../../../package.json').version
const resolvers = {
Query: {
serverVersion: () => serverVersion
}
serverVersion: () => serverVersion,
},
}
module.exports = resolvers

Some files were not shown because too many files have changed in this diff Show more