diff --git a/lib/notifier/email.js b/lib/notifier/email.js index 01ad5504..e7f7417c 100644 --- a/lib/notifier/email.js +++ b/lib/notifier/email.js @@ -1,5 +1,8 @@ const _ = require('lodash/fp') const prettyMs = require('pretty-ms') + +const email = require('../email') + const { PING, STALE, @@ -97,4 +100,7 @@ function emailAlert(alert) { } } -module.exports = { alertSubject, printEmailAlerts } +const sendMessage = email.sendMessage + + +module.exports = { alertSubject, printEmailAlerts, sendMessage } diff --git a/lib/notifier/index.js b/lib/notifier/index.js index 1905a5d3..292afdb3 100644 --- a/lib/notifier/index.js +++ b/lib/notifier/index.js @@ -1,12 +1,16 @@ -const { STALE, STALE_STATE } = require('./codes') - const _ = require('lodash/fp') -const queries = require('./queries') + +const configManager = require('../new-config-manager') const logger = require('../logger') +const machineLoader = require('../machine-loader') +const queries = require('./queries') +const settingsLoader = require('../new-settings-loader') +const customers = require('../customers') const utils = require('./utils') const emailFuncs = require('./email') const smsFuncs = require('./sms') +const { STALE, STALE_STATE } = require('./codes') function buildMessage(alerts, notifications) { const smsEnabled = utils.isActive(notifications.sms) @@ -132,8 +136,79 @@ function checkStuckScreen(deviceEvents, machineName) { return [] } +async function transactionNotify (tx, rec) { + const settings = await settingsLoader.loadLatest() + const notifSettings = configManager.getGlobalNotifications(settings.config) + const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity) + const isCashOut = tx.direction === 'cashOut' + + // high value tx on database + if(highValueTx) { + queries.addHighValueTx(tx) + } + + // alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled + const cashOutConfig = configManager.getCashOut(tx.deviceId, settings.config) + const zeroConfLimit = cashOutConfig.zeroConfLimit + const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit) + const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions + const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({}) + + if (!notificationsEnabled && !highValueTx) return Promise.resolve() + if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve() + if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error) + + return Promise.all([ + machineLoader.getMachineName(tx.deviceId), + customerPromise + ]) + .then(([machineName, customer]) => { + return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer) + }) + .then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx)) +} + +function sendRedemptionMessage(txId, error) { + const subject = `Here's an update on transaction ${txId}` + const body = error + ? `Error: ${error}` + : 'It was just dispensed successfully' + + const rec = { + sms: { + body: `${subject} - ${body}` + }, + email: { + subject, + body + } + } + return sendTransactionMessage(rec) +} + +async function sendTransactionMessage(rec, isHighValueTx) { + const settings = await settingsLoader.loadLatest() + const notifications = configManager.getGlobalNotifications(settings.config) + + let promises = [] + + const emailActive = + notifications.email.active && + (notifications.email.transactions || isHighValueTx) + if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec)) + + const smsActive = + notifications.sms.active && + (notifications.sms.transactions || isHighValueTx) + if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec)) + + return Promise.all(promises) +} + module.exports = { + transactionNotify, checkNotification, checkPings, - checkStuckScreen + checkStuckScreen, + sendRedemptionMessage } diff --git a/lib/notifier/queries.js b/lib/notifier/queries.js index a41c3888..f7c3d66a 100644 --- a/lib/notifier/queries.js +++ b/lib/notifier/queries.js @@ -1,3 +1,12 @@ const dbm = require('../postgresql_interface') +const db = require('../db') +const { v4: uuidv4 } = require('uuid') -module.exports = { machineEvents: dbm.machineEvents } +const addHighValueTx = (tx) => { + const sql = `INSERT INTO notifications (id, type, device_id, message, created) values($1, $2, $3, $4, CURRENT_TIMESTAMP)` + const direction = tx.direction === "cashOut" ? 'cash-out' : 'cash-in' + const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction` + return db.oneOrNone(sql, [uuidv4(), 'highValueTransaction', tx.deviceId, message]) +} + +module.exports = { machineEvents: dbm.machineEvents, addHighValueTx } diff --git a/lib/notifier/sms.js b/lib/notifier/sms.js index ea9a2c93..f4e041af 100644 --- a/lib/notifier/sms.js +++ b/lib/notifier/sms.js @@ -1,5 +1,6 @@ const _ = require('lodash/fp') const utils = require('./utils') +const sms = require('../sms') function printSmsAlerts(alertRec, config) { let alerts = [] @@ -50,4 +51,6 @@ function printSmsAlerts(alertRec, config) { return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ') } -module.exports = { printSmsAlerts } +const sendMessage = sms.sendMessage + +module.exports = { printSmsAlerts, sendMessage } diff --git a/lib/notifier/test/notifier.test.js b/lib/notifier/test/notifier.test.js index c7a6f4f8..f7314af8 100644 --- a/lib/notifier/test/notifier.test.js +++ b/lib/notifier/test/notifier.test.js @@ -1,4 +1,15 @@ +const BigNumber = require('../../../lib/bn') + const notifier = require('..') +const utils = require('../utils') +const queries = require("../queries") +const emailFuncs = require('../email') +const smsFuncs = require('../sms') + +afterEach(() => { + // https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm + jest.restoreAllMocks() +}) // mock plugins object with mock data to test functions const plugins = { @@ -33,32 +44,57 @@ const plugins = { checkBalances: () => [] } -const devices = [ - { - deviceId: - '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', - lastPing: '2020-11-16T13:11:03.169Z', - name: 'Abc123' +const tx = { + id: 'bec8d452-9ea2-4846-841b-55a9df8bbd00', + deviceId: + '490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88', + toAddress: 'bc1q7s4yy5n9vp6zhlf6mrw3cttdgx5l3ysr2mhc4v', + cryptoAtoms: BigNumber(252100), + cryptoCode: 'BTC', + fiat: BigNumber(55), + fiatCode: 'USD', + fee: null, + txHash: null, + phone: null, + error: null, + created: '2020-12-04T16:28:11.016Z', + send: true, + sendConfirmed: false, + timedout: false, + sendTime: null, + errorCode: null, + operatorCompleted: false, + sendPending: true, + cashInFee: BigNumber(2), + cashInFeeCrypto: BigNumber(9500), + minimumTx: 5, + customerId: '47ac1184-8102-11e7-9079-8f13a7117867', + txVersion: 6, + termsAccepted: false, + commissionPercentage: BigNumber(0.11), + rawTickerPrice: BigNumber(18937.4), + isPaperWallet: false, + direction: 'cashIn' +} + +const notifSettings = { + email_active: false, + sms_active: true, + email_errors: false, + sms_errors: true, + sms_transactions: true, + highValueTransaction: Infinity, //this will make highValueTx always false + sms: { + active: true, + errors: true, + transactions: false // force early return }, - { - deviceId: - '9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7', - lastPing: '2020-11-16T16:21:35.948Z', - name: 'Machine 2' - }, - { - deviceId: - '5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913', - lastPing: '2020-11-19T15:07:57.089Z', - name: 'Machine 3' - }, - { - deviceId: - 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - lastPing: '2020-11-23T19:34:41.031Z', - name: 'New Machine 4 ' + email: { + active: false, + errors: false, + transactions: false // force early return } -] +} test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => { expect.assertions(1) @@ -116,17 +152,11 @@ test('Checkpings returns empty array as the value for the id prop, if the lastPi }) }) -afterEach(() => { - // https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm - jest.restoreAllMocks() -}) - -const utils = require('../utils') test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => { const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert') mockShouldNotAlert.mockReturnValue(true) - + const result = await notifier.checkNotification(plugins) expect(mockShouldNotAlert).toHaveBeenCalledTimes(1) expect(result).toBe(undefined) @@ -160,7 +190,6 @@ test('If no alert fingerprint and inAlert is true, exits on call to sendNoAlerts expect(mockSendNoAlerts).toHaveBeenCalledTimes(1) }) - // vvv tests for checkstuckscreen... test('checkStuckScreen returns [] when no events are found', () => { expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([]) @@ -168,67 +197,141 @@ test('checkStuckScreen returns [] when no events are found', () => { test('checkStuckScreen returns [] if most recent event is idle', () => { // device_time is what matters for the sorting of the events by recency - expect(notifier.checkStuckScreen([{ - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":false}', - created: "2020-11-23T19:30:29.209Z", - device_time: "1999-11-23T19:30:29.177Z", - age: 157352628.123 - }, { - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":true}', - created: "2020-11-23T19:30:29.209Z", - device_time: "2020-11-23T19:30:29.177Z", - age: 157352628.123 - }])).toEqual([]) + expect( + notifier.checkStuckScreen([ + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: '2020-11-23T19:30:29.209Z', + device_time: '1999-11-23T19:30:29.177Z', + age: 157352628.123 + }, + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":true}', + created: '2020-11-23T19:30:29.209Z', + device_time: '2020-11-23T19:30:29.177Z', + age: 157352628.123 + } + ]) + ).toEqual([]) }) test('checkStuckScreen returns object array of length 1 with prop code: "STALE" if age > STALE_STATE', () => { // there is an age 0 and an isIdle true in the first object but it will be below the second one in the sorting order and thus ignored - const result = notifier.checkStuckScreen([{ - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":true}', - created: "2020-11-23T19:30:29.209Z", - device_time: "1999-11-23T19:30:29.177Z", - age: 0 - }, { - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":false}', - created: "2020-11-23T19:30:29.209Z", - device_time: "2020-11-23T19:30:29.177Z", - age: 157352628.123 - }]) - expect(result[0]).toMatchObject({code: "STALE"}) + const result = notifier.checkStuckScreen([ + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":true}', + created: '2020-11-23T19:30:29.209Z', + device_time: '1999-11-23T19:30:29.177Z', + age: 0 + }, + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: '2020-11-23T19:30:29.209Z', + device_time: '2020-11-23T19:30:29.177Z', + age: 157352628.123 + } + ]) + expect(result[0]).toMatchObject({ code: 'STALE' }) }) test('checkStuckScreen returns empty array if age < STALE_STATE', () => { - const STALE_STATE = require("../codes").STALE_STATE - const result1 = notifier.checkStuckScreen([{ - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":false}', - created: "2020-11-23T19:30:29.209Z", - device_time: "2020-11-23T19:30:29.177Z", - age: 0 - }]) - const result2 = notifier.checkStuckScreen([{ - id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', - device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', - event_type: 'stateChange', - note: '{"state":"chooseCoin","isIdle":false}', - created: "2020-11-23T19:30:29.209Z", - device_time: "2020-11-23T19:30:29.177Z", - age: STALE_STATE - }]) + const STALE_STATE = require('../codes').STALE_STATE + const result1 = notifier.checkStuckScreen([ + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: '2020-11-23T19:30:29.209Z', + device_time: '2020-11-23T19:30:29.177Z', + age: 0 + } + ]) + const result2 = notifier.checkStuckScreen([ + { + id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', + device_id: + 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', + event_type: 'stateChange', + note: '{"state":"chooseCoin","isIdle":false}', + created: '2020-11-23T19:30:29.209Z', + device_time: '2020-11-23T19:30:29.177Z', + age: STALE_STATE + } + ]) expect(result1).toEqual([]) expect(result2).toEqual([]) +}) + +test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () => { + const configManager = require('../../new-config-manager') + const settingsLoader = require('../../new-settings-loader') + + const loadLatest = jest.spyOn(settingsLoader, 'loadLatest') + const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications') + const getCashOut = jest.spyOn(configManager, 'getCashOut') + + // sendRedemptionMessage will cause this func to be called + jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec) + + getCashOut.mockReturnValue({zeroConfLimit: -Infinity}) + loadLatest.mockReturnValue({}) + getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }}) + + const response = await notifier.transactionNotify(tx, {isRedemption: true}) + + // this type of response implies sendRedemptionMessage was called + expect(response[0]).toMatchObject({ + sms: { + body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully" + }, + email: { + subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00", + body: 'It was just dispensed successfully' + } + }) +}) + +test("calls sendTransactionMessage if !zeroConf and !rec.isRedemption", async () => { + const configManager = require('../../new-config-manager') + const settingsLoader = require('../../new-settings-loader') + const machineLoader = require('../../machine-loader') + + const loadLatest = jest.spyOn(settingsLoader, 'loadLatest') + const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications') + const getCashOut = jest.spyOn(configManager, 'getCashOut') + const getMachineName = jest.spyOn(machineLoader, 'getMachineName') + const buildTransactionMessage = jest.spyOn(utils, 'buildTransactionMessage') + + // sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue + jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({prop: rec})) + buildTransactionMessage.mockImplementation(() => ["mock message", false]) + + getMachineName.mockReturnValue("mockMachineName") + getCashOut.mockReturnValue({zeroConfLimit: -Infinity}) + loadLatest.mockReturnValue({}) + getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }}) + + const response = await notifier.transactionNotify(tx, {isRedemption: false}) + + // If the return object is this, it means the code went through all the functions expected to go through if + // getMachineName, buildTransactionMessage and sendTransactionMessage were called, in this order + expect(response).toEqual([{prop: 'mock message'}]) }) \ No newline at end of file diff --git a/lib/notifier/utils.js b/lib/notifier/utils.js index c3ef184f..ca92a4aa 100644 --- a/lib/notifier/utils.js +++ b/lib/notifier/utils.js @@ -1,5 +1,7 @@ const _ = require('lodash/fp') const crypto = require('crypto') + +const coinUtils = require('../coin-utils') const { CODES_DISPLAY, NETWORK_DOWN_TIME, @@ -96,6 +98,51 @@ function sendNoAlerts(plugins, smsEnabled, emailEnabled) { return plugins.sendMessage(rec) } +const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) => { + const isCashOut = tx.direction === 'cashOut' + const direction = isCashOut ? 'Cash Out' : 'Cash In' + const crypto = `${coinUtils.toUnit(tx.cryptoAtoms, tx.cryptoCode)} ${ + tx.cryptoCode + }` + const fiat = `${tx.fiat} ${tx.fiatCode}` + const customerName = customer.name || customer.id + const phone = customer.phone ? `- Phone: ${customer.phone}` : '' + + let status = null + if (rec.error) { + status = `Error - ${rec.error}` + } else { + status = !isCashOut + ? 'Successful' + : !rec.isRedemption + ? 'Successful & awaiting redemption' + : 'Successful & dispensed' + } + + const body = ` + - Transaction ID: ${tx.id} + - Status: ${status} + - Machine name: ${machineName} + - ${direction} + - ${fiat} + - ${crypto} + - Customer: ${customerName} + ${phone} +` + const smsSubject = `A ${highValueTx ? 'high value ' : ''}${direction.toLowerCase()} transaction just happened at ${machineName} for ${fiat}` + const emailSubject = `A ${highValueTx ? 'high value ' : ''}transaction just happened` + + return [{ + sms: { + body: `${smsSubject} – ${status}` + }, + email: { + emailSubject, + body + } + }, highValueTx] +} + module.exports = { codeDisplay, parseEventNote, @@ -107,5 +154,6 @@ module.exports = { setAlertFingerprint, shouldNotAlert, buildAlertFingerprint, - sendNoAlerts + sendNoAlerts, + buildTransactionMessage } diff --git a/lib/plugins.js b/lib/plugins.js index e61bccc3..a8e3af0d 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -1,4 +1,3 @@ -const uuid = require('uuid') const _ = require('lodash/fp') const argv = require('minimist')(process.argv.slice(2)) const crypto = require('crypto') @@ -24,6 +23,8 @@ const coinUtils = require('./coin-utils') const commissionMath = require('./commission-math') const promoCodes = require('./promo-codes') +const notifier = require('./notifier/index') + const mapValuesWithKey = _.mapValues.convert({ cap: false }) @@ -54,7 +55,8 @@ function plugins (settings, deviceId) { ? undefined : BN(1).add(BN(commissions.cashOut).div(100)) - if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) + if (Date.now() - rateRec.timestamp > STALE_TICKER) + return logger.warn('Stale rate for ' + cryptoCode) const rate = rateRec.rates withCommission ? rates[cryptoCode] = { @@ -88,8 +90,10 @@ function plugins (settings, deviceId) { cryptoCodes.forEach((cryptoCode, i) => { const balanceRec = balanceRecs[i] - if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet') - if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode) + if (!balanceRec) + return logger.warn('No balance for ' + cryptoCode + ' yet') + if (Date.now() - balanceRec.timestamp > STALE_BALANCE) + return logger.warn('Stale balance for ' + cryptoCode) balances[cryptoCode] = balanceRec.balance }) @@ -109,10 +113,13 @@ function plugins (settings, deviceId) { const sumTxs = (sum, tx) => { const bills = tx.bills const sameDenominations = a => a[0].denomination === a[1].denomination - const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills)) + const doDenominationsMatch = _.every( + sameDenominations, + _.zip(cassettes, bills) + ) if (!doDenominationsMatch) { - throw new Error('Denominations don\'t add up, cassettes were changed.') + throw new Error("Denominations don't add up, cassettes were changed.") } return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills)) @@ -155,30 +162,41 @@ function plugins (settings, deviceId) { ? argv.cassettes.split(',') : rec.counts - const cassettes = [ - { - denomination: parseInt(denominations[0], 10), - count: parseInt(counts[0], 10) - }, - { - denomination: parseInt(denominations[1], 10), - count: parseInt(counts[1], 10) - } - ] + return Promise.all([ + dbm.cassetteCounts(deviceId), + cashOutHelper.redeemableTxs(deviceId, excludeTxId) + ]).then(([rec, _redeemableTxs]) => { + const redeemableTxs = _.reject( + _.matchesProperty('id', excludeTxId), + _redeemableTxs + ) - try { - return { - cassettes: computeAvailableCassettes(cassettes, redeemableTxs), - virtualCassettes - } - } catch (err) { - logger.error(err) - return { - cassettes, - virtualCassettes - } + const counts = argv.cassettes ? argv.cassettes.split(',') : rec.counts + + const cassettes = [ + { + denomination: parseInt(denominations[0], 10), + count: parseInt(counts[0], 10) + }, + { + denomination: parseInt(denominations[1], 10), + count: parseInt(counts[1], 10) } - }) + ] + + try { + return { + cassettes: computeAvailableCassettes(cassettes, redeemableTxs), + virtualCassettes + } + } catch (err) { + logger.error(err) + return { + cassettes, + virtualCassettes + } + } + }) } function fetchCurrentConfigVersion () { @@ -188,18 +206,23 @@ function plugins (settings, deviceId) { order by id desc limit 1` - return db.one(sql, ['config']) - .then(row => row.id) + return db.one(sql, ['config']).then(row => row.id) } function mapCoinSettings (coinParams) { const cryptoCode = coinParams[0] const cryptoNetwork = coinParams[1] - const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + const commissions = configManager.getCommissions( + cryptoCode, + deviceId, + settings.config + ) const minimumTx = BN(commissions.minimumTx) const cashInFee = BN(commissions.fixedFee) const cashInCommission = BN(commissions.cashIn) - const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null + const cashOutCommission = _.isNumber(commissions.cashOut) + ? BN(commissions.cashOut) + : null const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) return { @@ -213,15 +236,25 @@ function plugins (settings, deviceId) { } } - function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) { + function pollQueries ( + serialNumber, + deviceTime, + deviceRec, + machineVersion, + machineModel + ) { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies - const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) + const tickerPromises = cryptoCodes.map(c => + ticker.getRates(settings, fiatCode, c) + ) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) - const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c)) + const testnetPromises = cryptoCodes.map(c => + wallet.cryptoNetwork(settings, c) + ) const pingPromise = recordPing(deviceTime, machineVersion, machineModel) const currentConfigVersionPromise = fetchCurrentConfigVersion() const currentAvailablePromoCodes = promoCodes.getNumberOfAvailablePromoCodes() @@ -256,7 +289,12 @@ function plugins (settings, deviceId) { } function sendCoins (tx) { - return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode) + return wallet.sendCoins( + settings, + tx.toAddress, + tx.cryptoAtoms, + tx.cryptoCode + ) } function recordPing (deviceTime, version, model) { @@ -267,11 +305,18 @@ function plugins (settings, deviceId) { } return Promise.all([ - db.none(`insert into machine_pings(device_id, device_time) values($1, $2) - ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime]), - db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', { - deviceId - }) + db.none( + `insert into machine_pings(device_id, device_time) values($1, $2) + ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, + [deviceId, deviceTime] + ), + db.none( + pgp.helpers.update(devices, null, 'devices') + + 'WHERE device_id = ${deviceId}', + { + deviceId + } + ) ]) } @@ -303,34 +348,37 @@ function plugins (settings, deviceId) { } function fiatBalance (fiatCode, cryptoCode) { - const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + const commissions = configManager.getCommissions( + cryptoCode, + deviceId, + settings.config + ) return Promise.all([ ticker.getRates(settings, fiatCode, cryptoCode), wallet.balance(settings, cryptoCode) - ]) - .then(([rates, balanceRec]) => { - if (!rates || !balanceRec) return null + ]).then(([rates, balanceRec]) => { + if (!rates || !balanceRec) return null - const rawRate = rates.rates.ask - const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100)) - const balance = balanceRec.balance + const rawRate = rates.rates.ask + const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100)) + const balance = balanceRec.balance - if (!rawRate || !balance) return null + if (!rawRate || !balance) return null - const rate = rawRate.div(cashInCommission) + const rate = rawRate.div(cashInCommission) - const lowBalanceMargin = BN(1.03) + const lowBalanceMargin = BN(1.03) - const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) - const unitScale = cryptoRec.unitScale - const shiftedRate = rate.shift(-unitScale) - const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin) + const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) + const unitScale = cryptoRec.unitScale + const shiftedRate = rate.shift(-unitScale) + const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin) - return { - timestamp: balanceRec.timestamp, - balance: fiatTransferBalance.truncated().toString() - } - }) + return { + timestamp: balanceRec.timestamp, + balance: fiatTransferBalance.truncated().toString() + } + }) } function notifyConfirmation (tx) { @@ -345,100 +393,31 @@ function plugins (settings, deviceId) { } } - return sms.sendMessage(settings, rec) - .then(() => { - const sql = 'update cash_out_txs set notified=$1 where id=$2' - const values = [true, tx.id] + return sms.sendMessage(settings, rec).then(() => { + const sql = 'update cash_out_txs set notified=$1 where id=$2' + const values = [true, tx.id] - return db.none(sql, values) - }) + return db.none(sql, values) + }) } function notifyOperator (tx, rec) { - const notifications = configManager.getGlobalNotifications(settings.config) - - const notificationsEnabled = notifications.sms.transactions || notifications.email.transactions - const highValueTx = tx.fiat.gt(notifications.highValueTransaction || Infinity) - - if (!notificationsEnabled && !highValueTx) return Promise.resolve() - - const isCashOut = tx.direction === 'cashOut' - const zeroConf = isCashOut && isZeroConf(tx) - - // 0-conf cash-out should only send notification on redemption - if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve() - - if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error) - - const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({}) - - return Promise.all([machineLoader.getMachineName(tx.deviceId), customerPromise]) - .then(([machineName, customer]) => { - const direction = isCashOut ? 'Cash Out' : 'Cash In' - const crypto = `${coinUtils.toUnit(tx.cryptoAtoms, tx.cryptoCode)} ${tx.cryptoCode}` - const fiat = `${tx.fiat} ${tx.fiatCode}` - const customerName = customer.name || customer.id - const phone = customer.phone ? `- Phone: ${customer.phone}` : '' - - let status - if (rec.error) { - status = `Error - ${rec.error}` - } else { - status = !isCashOut ? 'Successful' : !rec.isRedemption - ? 'Successful & awaiting redemption' : 'Successful & dispensed' - } - - const body = ` - - Transaction ID: ${tx.id} - - Status: ${status} - - Machine name: ${machineName} - - ${direction} - - ${fiat} - - ${crypto} - - Customer: ${customerName} - ${phone} - ` - const smsSubject = `A ${highValueTx ? 'high value ' : ''}${direction.toLowerCase()} transaction just happened at ${machineName} for ${fiat}` - const emailSubject = `A ${highValueTx ? 'high value ' : ''}transaction just happened` - - return [{ - sms: { - body: `${smsSubject} – ${status}` - }, - email: { - emailSubject, - body - } - }, highValueTx] - }) - .then(([rec, highValueTx]) => sendTransactionMessage(rec, highValueTx)) - } - - function sendRedemptionMessage (txId, error) { - const subject = `Here's an update on transaction ${txId}` - const body = error ? `Error: ${error}` : 'It was just dispensed successfully' - - const rec = { - sms: { - body: `${subject} - ${body}` - }, - email: { - subject, - body - } - } - return sendTransactionMessage(rec) + // notify operator about new transaction and add high volume txs to database + return notifier.transactionNotify(tx, rec) } function clearOldLogs () { - return logs.clearOldLogs() - .catch(logger.error) + return logs.clearOldLogs().catch(logger.error) } function pong () { - return db.none(`UPDATE server_events SET created=now() WHERE event_type=$1; + return db + .none( + `UPDATE server_events SET created=now() WHERE event_type=$1; INSERT INTO server_events (event_type) SELECT $1 - WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, ['ping']) + WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, + ['ping'] + ) .catch(logger.error) } @@ -479,15 +458,18 @@ function plugins (settings, deviceId) { const marketTradesQueues = tradesQueues[market] if (!marketTradesQueues || marketTradesQueues.length === 0) return null - logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length) + logger.debug( + '[%s] tradesQueues size: %d', + market, + marketTradesQueues.length + ) logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0]) const t1 = Date.now() - const filtered = marketTradesQueues - .filter(tradeEntry => { - return t1 - tradeEntry.timestamp < TRADE_TTL - }) + const filtered = marketTradesQueues.filter(tradeEntry => { + return t1 - tradeEntry.timestamp < TRADE_TTL + }) const filteredCount = marketTradesQueues.length - filtered.length @@ -498,10 +480,14 @@ function plugins (settings, deviceId) { if (filtered.length === 0) return null - const cryptoAtoms = filtered - .reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0)) + const cryptoAtoms = filtered.reduce( + (prev, current) => prev.plus(current.cryptoAtoms), + BN(0) + ) - const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0) + const timestamp = filtered + .map(r => r.timestamp) + .reduce((acc, r) => Math.max(acc, r), 0) const consolidatedTrade = { fiatCode, @@ -517,11 +503,15 @@ function plugins (settings, deviceId) { } function executeTrades () { - return machineLoader.getMachines() + return machineLoader + .getMachines() .then(devices => { const deviceIds = devices.map(device => device.deviceId) const lists = deviceIds.map(deviceId => { - const localeConfig = configManager.getLocale(deviceId, settings.config) + const localeConfig = configManager.getLocale( + deviceId, + settings.config + ) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies @@ -531,8 +521,9 @@ function plugins (settings, deviceId) { })) }) - const tradesPromises = _.uniq(_.flatten(lists)) - .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)) + const tradesPromises = _.uniq(_.flatten(lists)).map(r => + executeTradesForMarket(settings, r.fiatCode, r.cryptoCode) + ) return Promise.all(tradesPromises) }) @@ -547,41 +538,43 @@ function plugins (settings, deviceId) { if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return - return executeTradeForType(tradeEntry) - .catch(err => { - tradesQueues[market].push(tradeEntry) - if (err.name === 'orderTooSmall') return logger.debug(err.message) - logger.error(err) - }) + return executeTradeForType(tradeEntry).catch(err => { + tradesQueues[market].push(tradeEntry) + if (err.name === 'orderTooSmall') return logger.debug(err.message) + logger.error(err) + }) } function executeTradeForType (_tradeEntry) { - const expand = te => _.assign(te, { - cryptoAtoms: te.cryptoAtoms.abs(), - type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell' - }) + const expand = te => + _.assign(te, { + cryptoAtoms: te.cryptoAtoms.abs(), + type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell' + }) const tradeEntry = expand(_tradeEntry) const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell - return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode) + return execute( + settings, + tradeEntry.cryptoAtoms, + tradeEntry.fiatCode, + tradeEntry.cryptoCode + ) .then(() => recordTrade(tradeEntry)) .catch(err => { - return recordTrade(tradeEntry, err) - .then(() => { - throw err - }) + return recordTrade(tradeEntry, err).then(() => { + throw err + }) }) } function convertBigNumFields (obj) { - const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat']) - ? value.toString() - : value + const convert = (value, key) => + _.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : 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)) } @@ -611,22 +604,10 @@ function plugins (settings, deviceId) { const notifications = configManager.getGlobalNotifications(settings.config) let promises = [] - if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec)) - if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec)) - - return Promise.all(promises) - } - - function sendTransactionMessage (rec, isHighValueTx) { - const notifications = configManager.getGlobalNotifications(settings.config) - - let promises = [] - - const emailActive = notifications.email.active && (notifications.email.transactions || isHighValueTx) - if (emailActive) promises.push(email.sendMessage(settings, rec)) - - const smsActive = notifications.sms.active && (notifications.sms.transactions || isHighValueTx) - if (smsActive) promises.push(sms.sendMessage(settings, rec)) + if (notifications.email.active && rec.email) + promises.push(email.sendMessage(settings, rec)) + if (notifications.sms.active && rec.sms) + promises.push(sms.sendMessage(settings, rec)) return Promise.all(promises) } @@ -636,53 +617,64 @@ function plugins (settings, deviceId) { } function checkDeviceCashBalances (fiatCode, device) { - const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config) + const cashOutConfig = configManager.getCashOut( + device.deviceId, + settings.config + ) const denomination1 = cashOutConfig.top const denomination2 = cashOutConfig.bottom const cashOutEnabled = cashOutConfig.active - const notifications = configManager.getNotifications(null, device.deviceId, settings.config) + const notifications = configManager.getNotifications( + null, + device.deviceId, + settings.config + ) const machineName = device.name - const cashInAlert = device.cashbox > notifications.cashInAlertThreshold - ? { - code: 'CASH_BOX_FULL', - machineName, - deviceId: device.deviceId, - notes: device.cashbox - } - : null + const cashInAlert = + device.cashbox > notifications.cashInAlertThreshold + ? { + code: 'CASH_BOX_FULL', + machineName, + deviceId: device.deviceId, + notes: device.cashbox + } + : null - const cassette1Alert = cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1 - ? { - code: 'LOW_CASH_OUT', - cassette: 1, - machineName, - deviceId: device.deviceId, - notes: device.cassette1, - denomination: denomination1, - fiatCode - } - : null + const cassette1Alert = + cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1 + ? { + code: 'LOW_CASH_OUT', + cassette: 1, + machineName, + deviceId: device.deviceId, + notes: device.cassette1, + denomination: denomination1, + fiatCode + } + : null - const cassette2Alert = cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2 - ? { - code: 'LOW_CASH_OUT', - cassette: 2, - machineName, - deviceId: device.deviceId, - notes: device.cassette2, - denomination: denomination2, - fiatCode - } - : null + const cassette2Alert = + cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2 + ? { + code: 'LOW_CASH_OUT', + cassette: 2, + machineName, + deviceId: device.deviceId, + notes: device.cassette2, + denomination: denomination2, + fiatCode + } + : null return _.compact([cashInAlert, cassette1Alert, cassette2Alert]) } function checkCryptoBalances (fiatCode, devices) { - const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes) + const fiatBalancePromises = cryptoCodes => + _.map(c => fiatBalance(fiatCode, c), cryptoCodes) const fetchCryptoCodes = _deviceId => { const localeConfig = configManager.getLocale(_deviceId, settings.config) @@ -693,8 +685,9 @@ function plugins (settings, deviceId) { const cryptoCodes = union(devices) const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode]) - return Promise.all(fiatBalancePromises(cryptoCodes)) - .then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances))) + return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances => + _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)) + ) } function checkCryptoBalance (fiatCode, rec) { @@ -702,20 +695,30 @@ function plugins (settings, deviceId) { if (!fiatBalance) return null - const notifications = configManager.getNotifications(cryptoCode, null, settings.config) + const notifications = configManager.getNotifications( + cryptoCode, + null, + settings.config + ) const lowAlertThreshold = notifications.cryptoLowBalance const highAlertThreshold = notifications.cryptoHighBalance const req = { cryptoCode, fiatBalance, - fiatCode, + fiatCode } - if (_.isFinite(lowAlertThreshold) && BN(fiatBalance.balance).lt(lowAlertThreshold)) + if ( + _.isFinite(lowAlertThreshold) && + BN(fiatBalance.balance).lt(lowAlertThreshold) + ) return _.set('code')('LOW_CRYPTO_BALANCE')(req) - if (_.isFinite(highAlertThreshold) && BN(fiatBalance.balance).gt(highAlertThreshold)) + if ( + _.isFinite(highAlertThreshold) && + BN(fiatBalance.balance).gt(highAlertThreshold) + ) return _.set('code')('HIGH_CRYPTO_BALANCE')(req) return null @@ -725,24 +728,23 @@ function plugins (settings, deviceId) { const localeConfig = configManager.getGlobalLocale(settings.config) const fiatCode = localeConfig.fiatCurrency - return machineLoader.getMachines() - .then(devices => { - return Promise.all([ - checkCryptoBalances(fiatCode, devices), - checkDevicesCashBalances(fiatCode, devices) - ]) - .then(_.flow(_.flattenDeep, _.compact)) - }) + return machineLoader.getMachines().then(devices => { + return Promise.all([ + checkCryptoBalances(fiatCode, devices), + checkDevicesCashBalances(fiatCode, devices) + ]).then(_.flow(_.flattenDeep, _.compact)) + }) } function randomCode () { - return BN(crypto.randomBytes(3).toString('hex'), 16).shift(-6).toFixed(6).slice(-6) + return BN(crypto.randomBytes(3).toString('hex'), 16) + .shift(-6) + .toFixed(6) + .slice(-6) } function getPhoneCode (phone) { - const code = argv.mockSms - ? '123' - : randomCode() + const code = argv.mockSms ? '123' : randomCode() const rec = { sms: { @@ -751,14 +753,14 @@ function plugins (settings, deviceId) { } } - return sms.sendMessage(settings, rec) - .then(() => code) + return sms.sendMessage(settings, rec).then(() => code) } function sweepHdRow (row) { const cryptoCode = row.crypto_code - return wallet.sweep(settings, cryptoCode, row.hd_index) + return wallet + .sweep(settings, cryptoCode, row.hd_index) .then(txHash => { if (txHash) { logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash) @@ -769,14 +771,17 @@ function plugins (settings, deviceId) { return db.none(sql, row.id) } }) - .catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message)) + .catch(err => + logger.error('[%s] Sweep error: %s', cryptoCode, err.message) + ) } function sweepHd () { const sql = `select id, crypto_code, hd_index from cash_out_txs where hd_index is not null and not swept and status in ('confirmed', 'instant')` - return db.any(sql) + return db + .any(sql) .then(rows => Promise.all(rows.map(sweepHdRow))) .catch(err => logger.error(err)) } @@ -790,14 +795,15 @@ function plugins (settings, deviceId) { const fiatCode = localeConfig.fiatCurrency const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config) - const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) + const tickerPromises = cryptoCodes.map(c => + ticker.getRates(settings, fiatCode, c) + ) return Promise.all(tickerPromises) } function getRates () { - return getRawRates() - .then(buildRates) + return getRawRates().then(buildRates) } return { diff --git a/package-lock.json b/package-lock.json index 17836eb6..e2ca4c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,24 +57,25 @@ } }, "@babel/core": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", - "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", + "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.1", + "@babel/generator": "^7.12.5", "@babel/helper-module-transforms": "^7.12.1", - "@babel/helpers": "^7.12.1", - "@babel/parser": "^7.12.3", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.1", - "@babel/types": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.9", + "@babel/types": "^7.12.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.2", "lodash": "^4.17.19", + "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, @@ -155,6 +156,15 @@ "@babel/types": "^7.12.11" }, "dependencies": { + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, "@babel/helper-validator-identifier": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", @@ -191,34 +201,6 @@ } } }, - "@babel/helper-get-function-arity": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", - "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", - "dev": true, - "requires": { - "@babel/types": "^7.12.10" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/types": { - "version": "7.12.12", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", - "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - } - } - }, "@babel/helper-module-imports": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", @@ -413,9 +395,9 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -542,28 +524,28 @@ } }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" @@ -596,12 +578,12 @@ } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" }, @@ -1343,9 +1325,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.15.tgz", - "integrity": "sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==", + "version": "7.0.16", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.16.tgz", + "integrity": "sha512-S63Dt4CZOkuTmpLGGWtT/mQdVORJOpx6SZWGVaP56dda/0Nx5nEe82K7/LAm8zYr6SfMq+1N2OreIOrHAx656w==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -1627,9 +1609,9 @@ } }, "@types/yargs": { - "version": "15.0.10", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz", - "integrity": "sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ==", + "version": "15.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.11.tgz", + "integrity": "sha512-jfcNBxHFYJ4nPIacsi3woz1+kvUO6s1CyeEhtnDHBjHUMNj5UlW2GynmnSgiJJEdNg9yW5C8lfoNRZrHGv5EqA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -7806,9 +7788,9 @@ } }, "is-core-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", - "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -8350,9 +8332,9 @@ } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yargs": { @@ -9331,9 +9313,9 @@ } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yargs": {