diff --git a/lib/app.js b/lib/app.js index f910164c..07e90abd 100644 --- a/lib/app.js +++ b/lib/app.js @@ -82,8 +82,6 @@ function startServer (settings) { : https.createServer(httpsServerOptions, routes.app) const port = argv.port || 3000 - const localPort = 3030 - const localServer = http.createServer(routes.localApp) if (devMode) logger.info('In dev mode') @@ -91,10 +89,6 @@ function startServer (settings) { logger.info('lamassu-server listening on port ' + port + ' ' + (devMode ? '(http)' : '(https)')) }) - - localServer.listen(localPort, 'localhost', () => { - logger.info('lamassu-server listening on local port ' + localPort) - }) }) } diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js new file mode 100644 index 00000000..eb628fe7 --- /dev/null +++ b/lib/graphql/resolvers.js @@ -0,0 +1,239 @@ +const _ = require('lodash/fp') +const nmd = require('nano-markdown') + +const { accounts: accountsConfig, countries, languages } = require('../new-admin/config') +const plugins = require('../plugins') +const configManager = require('../new-config-manager') +const customRequestQueries = require('../new-admin/services/customInfoRequests') +const state = require('../middlewares/state') + +const VERSION = require('../../package.json').version + +const urlsToPing = [ + `us.archive.ubuntu.com`, + `uk.archive.ubuntu.com`, + `za.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 + } +] + +const addSmthInfo = (dstField, srcFields) => smth => + smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity + +const addOperatorInfo = addSmthInfo( + 'operatorInfo', + ['name', 'phone', 'email', 'website', 'companyNumber'] +) + +const addReceiptInfo = addSmthInfo( + 'receiptInfo', + [ + 'sms', + 'operatorWebsite', + 'operatorEmail', + 'operatorPhone', + 'companyNumber', + 'machineLocation', + 'customerNameOrPhoneNumber', + 'exchangeRate', + 'addressQRCode', + ] +) + +/* TODO: Simplify this. */ +const buildTriggers = (allTriggers) => { + const normalTriggers = [] + const customTriggers = _.filter(o => { + if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o) + return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId) + }, allTriggers) + + return _.flow( + _.map(_.get('customInfoRequestId')), + customRequestQueries.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] + }) +} + +/* + * TODO: From `lib/routes/termsAndConditionsRoutes.js` -- remove this after + * terms are removed from the GraphQL API too. + */ +const massageTerms = terms => (terms.active && terms.text) ? ({ + delay: Boolean(terms.delay), + title: terms.title, + text: nmd(terms.text), + accept: terms.acceptButtonText, + cancel: terms.cancelButtonText, +}) : null + +const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => { + const massageCoins = _.map(_.pick([ + 'batchable', + 'cashInCommission', + 'cashInFee', + 'cashOutCommission', + 'cryptoCode', + 'cryptoNetwork', + 'cryptoUnits', + 'display', + 'minimumTx' + ])) + + const staticConf = _.flow( + _.pick([ + 'areThereAvailablePromoCodes', + 'coins', + 'configVersion', + 'timezone' + ]), + _.update('coins', massageCoins), + _.set('serverVersion', VERSION), + )(pq) + + return Promise.all([ + !!configManager.getCompliance(settings.config).enablePaperWalletOnly, + configManager.getTriggersAutomation(customRequestQueries.getCustomInfoRequests(), settings.config), + buildTriggers(configManager.getTriggers(settings.config)), + configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2', + configManager.getLocale(deviceId, settings.config), + configManager.getOperatorInfo(settings.config), + configManager.getReceipt(settings.config), + massageTerms(configManager.getTermsConditions(settings.config)), + !!configManager.getCashOut(deviceId, settings.config).active, + ]) + .then(([ + enablePaperWalletOnly, + triggersAutomation, + triggers, + hasLightning, + localeInfo, + operatorInfo, + receiptInfo, + terms, + twoWayMode, + ]) => + (currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ? + null : + _.flow( + _.assign({ + enablePaperWalletOnly, + triggersAutomation, + triggers, + hasLightning, + localeInfo: { + country: localeInfo.country, + languages: localeInfo.languages, + fiatCode: localeInfo.fiatCurrency + }, + machineInfo: { deviceId, deviceName }, + twoWayMode, + speedtestFiles, + urlsToPing, + terms, + }), + _.update('triggersAutomation', _.mapValues(_.eq('Automatic'))), + addOperatorInfo(operatorInfo), + addReceiptInfo(receiptInfo) + )(staticConf)) +} + + +const setZeroConfLimit = config => coin => + _.set( + 'zeroConfLimit', + configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit, + coin + ) + +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 + + state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) + + return _.flow( + _.pick(['balances', 'cassettes', 'coins', 'rates']), + + _.update('cassettes', massageCassettes), + + /* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */ + _.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])), + + /* [{ 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 }, ... } */ + ({ balances, cassettes, coins, rates }) => ({ + cassettes, + coins: _.flow( + _.reduce( + (ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret), + rates + ), + + /* { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } => [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] */ + _.toPairs, + + /* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */ + _.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)) + )(_.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), + )(pq) +} + + +const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) => + plugins(settings, deviceId) + .pollQueries() + .then(pq => ({ + static: staticConfig({ + currentConfigVersion, + deviceId, + deviceName, + pq, + settings, + }), + dynamic: dynamicConfig({ + deviceId, + operatorId, + pid, + pq, + settings, + }), + })) + +module.exports = { + Query: { + configs + } +} diff --git a/lib/graphql/server.js b/lib/graphql/server.js new file mode 100644 index 00000000..fda7d85c --- /dev/null +++ b/lib/graphql/server.js @@ -0,0 +1,27 @@ +const logger = require('../logger') + +const https = require('https') +const { ApolloServer } = require('apollo-server-express') + +const devMode = !!require('minimist')(process.argv.slice(2)).dev + +module.exports = new ApolloServer({ + typeDefs: require('./types'), + resolvers: require('./resolvers'), + context: ({ req, res }) => ({ + deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */ + deviceName: req.deviceName, /* lib/middlewares/authorize.js */ + operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */ + pid: req.query.pid, + settings: req.settings, /* lib/middlewares/populateSettings.js */ + }), + uploads: false, + playground: false, + introspection: false, + formatError: error => { + logger.error(error) + return error + }, + debug: devMode, + logger +}) diff --git a/lib/graphql/types.js b/lib/graphql/types.js new file mode 100644 index 00000000..a9318978 --- /dev/null +++ b/lib/graphql/types.js @@ -0,0 +1,151 @@ +const { gql } = require('apollo-server-express') +module.exports = gql` +type Coin { + cryptoCode: String! + display: String! + minimumTx: String! + cashInFee: String! + cashInCommission: String! + cashOutCommission: String! + cryptoNetwork: Boolean! + cryptoUnits: String! + batchable: Boolean! +} + +type LocaleInfo { + country: String! + fiatCode: String! + languages: [String!]! +} + +type OperatorInfo { + name: String! + phone: String! + email: String! + website: String! + companyNumber: String! +} + +type MachineInfo { + deviceId: String! + deviceName: String +} + +type ReceiptInfo { + sms: Boolean! + operatorWebsite: Boolean! + operatorEmail: Boolean! + operatorPhone: Boolean! + companyNumber: Boolean! + machineLocation: Boolean! + customerNameOrPhoneNumber: Boolean! + exchangeRate: Boolean! + addressQRCode: Boolean! +} + +type SpeedtestFile { + url: String! + size: Int! +} + +# True if automatic, False otherwise +type TriggersAutomation { + sanctions: Boolean! + idCardPhoto: Boolean! + idCardData: Boolean! + facephoto: Boolean! + usSsn: Boolean! +} + +type Trigger { + id: String! + customInfoRequestId: String! + direction: String! + requirement: String! + triggerType: String! + + suspensionDays: Int + threshold: Int + thresholdDays: Int +} + +type Terms { + delay: Boolean! + title: String! + text: String! + accept: String! + cancel: String! +} + +type StaticConfig { + configVersion: Int! + + areThereAvailablePromoCodes: Boolean! + coins: [Coin!]! + enablePaperWalletOnly: Boolean! + hasLightning: Boolean! + serverVersion: String! + timezone: Int! + twoWayMode: Boolean! + + localeInfo: LocaleInfo! + operatorInfo: OperatorInfo + machineInfo: MachineInfo! + receiptInfo: ReceiptInfo + + speedtestFiles: [SpeedtestFile!]! + urlsToPing: [String!]! + + triggersAutomation: TriggersAutomation! + triggers: [Trigger!]! + + terms: Terms +} + +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! + + # Raw rates + ask: String! + bid: String! + + # Rates with commissions applied + cashIn: String! + cashOut: String! + + zeroConfLimit: Int! +} + +type PhysicalCassette { + denomination: Int! + count: Int! +} + +type Cassettes { + physical: [PhysicalCassette!]! + virtual: [Int!]! +} + +type DynamicConfig { + cassettes: Cassettes + coins: [DynamicCoinValues!]! + reboot: Boolean! + shutdown: Boolean! + restartServices: Boolean! +} + +type Configs { + static: StaticConfig + dynamic: DynamicConfig! +} + +type Query { + configs(currentConfigVersion: Int): Configs! +} +` diff --git a/lib/machine-loader.js b/lib/machine-loader.js index ac6853e2..ec1f48a2 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -209,6 +209,7 @@ function setMachine (rec, operatorId) { } function updateNetworkPerformance (deviceId, data) { + if (_.isEmpty(data)) return Promise.resolve(true) const downloadSpeed = _.head(data) const dbData = { device_id: deviceId, @@ -224,6 +225,7 @@ function updateNetworkPerformance (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) const dbData = { diff --git a/lib/middlewares/recordPing.js b/lib/middlewares/recordPing.js new file mode 100644 index 00000000..e74de771 --- /dev/null +++ b/lib/middlewares/recordPing.js @@ -0,0 +1,7 @@ +const plugins = require('../plugins') + +module.exports = (req, res, next) => + plugins(req.settings, req.deviceId) + .recordPing(req.deviceTime, req.query.version, req.query.model) + .then(() => next()) + .catch(() => next()) diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js index 4c05b540..8951b2a4 100644 --- a/lib/new-config-manager.js +++ b/lib/new-config-manager.js @@ -48,7 +48,6 @@ const getCommissions = (cryptoCode, deviceId, config) => { const getLocale = (deviceId, it) => { const locale = fromNamespace(namespaces.LOCALE)(it) - const filter = _.matches({ machine: deviceId }) return _.omit('overrides', _.assignAll([locale, ..._.filter(filter)(locale.overrides)])) } diff --git a/lib/plugins.js b/lib/plugins.js index 80bbcc9b..0fbff479 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -25,6 +25,7 @@ const customers = require('./customers') const commissionMath = require('./commission-math') const loyalty = require('./loyalty') const transactionBatching = require('./tx-batching') +const state = require('./middlewares/state') const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants') @@ -39,7 +40,6 @@ const mapValuesWithKey = _.mapValues.convert({ const TRADE_TTL = 2 * T.minutes const STALE_TICKER = 3 * T.minutes const STALE_BALANCE = 3 * T.minutes -const PONG_TTL = '1 week' const tradesQueues = {} function plugins (settings, deviceId) { @@ -206,8 +206,7 @@ function plugins (settings, deviceId) { } function mapCoinSettings (coinParams) { - const cryptoCode = coinParams[0] - const cryptoNetwork = coinParams[1] + const [ cryptoCode, cryptoNetwork ] = coinParams const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) const minimumTx = new BN(commissions.minimumTx) const cashInFee = new BN(commissions.fixedFee) @@ -228,56 +227,57 @@ function plugins (settings, deviceId) { } } - function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) { + function pollQueries () { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies - const timezone = millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)) 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 pingPromise = recordPing(deviceTime, machineVersion, machineModel) - const currentConfigVersionPromise = fetchCurrentConfigVersion() - const currentAvailablePromoCodes = loyalty.getNumberOfAvailablePromoCodes() + const networkPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c)) const supportsBatchingPromise = cryptoCodes.map(c => wallet.supportsBatching(settings, c)) - const promises = [ + return Promise.all([ buildAvailableCassettes(), - pingPromise, - currentConfigVersionPromise, - timezone - ].concat( - supportsBatchingPromise, - tickerPromises, - balancePromises, - testnetPromises, - currentAvailablePromoCodes - ) + fetchCurrentConfigVersion(), + millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)), + loyalty.getNumberOfAvailablePromoCodes(), + Promise.all(supportsBatchingPromise), + Promise.all(tickerPromises), + Promise.all(balancePromises), + Promise.all(networkPromises) + ]) + .then(([ + cassettes, + configVersion, + timezone, + numberOfAvailablePromoCodes, + batchableCoins, + tickers, + balances, + networks + ]) => { + const coinsWithoutRate = _.flow( + _.zip(cryptoCodes), + _.map(mapCoinSettings) + )(networks) - return Promise.all(promises) - .then(arr => { - const cassettes = arr[0] - const configVersion = arr[2] - const tz = arr[3] - const cryptoCodesCount = cryptoCodes.length - const batchableCoinsRes = arr.slice(4, cryptoCodesCount + 4) - const batchableCoins = batchableCoinsRes.map(it => ({ batchable: it })) - const tickers = arr.slice(cryptoCodesCount + 4, 2 * cryptoCodesCount + 4) - const balances = arr.slice(2 * cryptoCodesCount + 4, 3 * cryptoCodesCount + 4) - const testNets = arr.slice(3 * cryptoCodesCount + 4, arr.length - 1) - const coinParams = _.zip(cryptoCodes, testNets) - const coinsWithoutRate = _.map(mapCoinSettings, coinParams) - const areThereAvailablePromoCodes = arr[arr.length - 1] > 0 + const coins = _.flow( + _.map(it => ({ batchable: it })), + _.zipWith( + _.assign, + _.zipWith(_.assign, coinsWithoutRate, tickers) + ) + )(batchableCoins) return { cassettes, rates: buildRates(tickers), balances: buildBalances(balances), - coins: _.zipWith(_.assign, _.zipWith(_.assign, coinsWithoutRate, tickers), batchableCoins), + coins, configVersion, - areThereAvailablePromoCodes, - timezone: tz + areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, + timezone } }) } @@ -365,12 +365,12 @@ function plugins (settings, deviceId) { const rate = rawRate.div(cashInCommission) - const lowBalanceMargin = new BN(1.05) + const lowBalanceMargin = new BN(0.95) const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale const shiftedRate = rate.shiftedBy(-unitScale) - const fiatTransferBalance = balance.times(shiftedRate).div(lowBalanceMargin) + const fiatTransferBalance = balance.times(shiftedRate).times(lowBalanceMargin) return { timestamp: balanceRec.timestamp, @@ -850,6 +850,7 @@ function plugins (settings, deviceId) { return { getRates, + recordPing, buildRates, getRawRates, buildRatesNoCommission, diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 4f74f811..080b2f18 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -1,49 +1,59 @@ -const ccxt = require('ccxt') - -const BN = require('../../bn') -const { buildMarket, verifyFiatSupport } = require('../common/ccxt') -const { getRate } = require('../../../lib/forex') - -const RETRIES = 2 - -function ticker (fiatCode, cryptoCode, tickerName) { - const ticker = new ccxt[tickerName]({ timeout: 3000 }) - if (verifyFiatSupport(fiatCode, tickerName)) { - return getCurrencyRates(ticker, fiatCode, cryptoCode) - } - - return getRate(RETRIES, fiatCode) - .then(({ fxRate }) => { - try { - return getCurrencyRates(ticker, 'USD', cryptoCode) - .then(res => ({ - rates: { - ask: res.rates.ask.times(fxRate), - bid: res.rates.bid.times(fxRate) - } - })) - } catch (e) { - return Promise.reject(e) - } - }) -} - -function getCurrencyRates (ticker, fiatCode, cryptoCode) { - try { - if (!ticker.has['fetchTicker']) { - throw new Error('Ticker not available') - } - const symbol = buildMarket(fiatCode, cryptoCode, ticker.id) - return ticker.fetchTicker(symbol) - .then(res => ({ - rates: { - ask: new BN(res.ask), - bid: new BN(res.bid) - } - })) - } catch (e) { - return Promise.reject(e) - } -} - -module.exports = { ticker } +const ccxt = require('ccxt') + +const BN = require('../../bn') +const { buildMarket, verifyFiatSupport } = require('../common/ccxt') +const { getRate } = require('../../../lib/forex') + +const RETRIES = 2 + +const tickerObjects = {} + +function ticker (fiatCode, cryptoCode, tickerName) { + if (!tickerObjects[tickerName]) { + tickerObjects[tickerName] = new ccxt[tickerName]({ + timeout: 3000, + enableRateLimit: false, + }) + } + + const ticker = tickerObjects[tickerName] + + if (verifyFiatSupport(fiatCode, tickerName)) { + return getCurrencyRates(ticker, fiatCode, cryptoCode) + } + + return getRate(RETRIES, fiatCode) + .then(({ fxRate }) => { + try { + return getCurrencyRates(ticker, 'USD', cryptoCode) + .then(res => ({ + rates: { + ask: res.rates.ask.times(fxRate), + bid: res.rates.bid.times(fxRate) + } + })) + } catch (e) { + return Promise.reject(e) + } + }) +} + +function getCurrencyRates (ticker, fiatCode, cryptoCode) { + try { + if (!ticker.has['fetchTicker']) { + throw new Error('Ticker not available') + } + const symbol = buildMarket(fiatCode, cryptoCode, ticker.id) + return ticker.fetchTicker(symbol) + .then(res => ({ + rates: { + ask: new BN(res.ask), + bid: new BN(res.bid) + } + })) + } catch (e) { + return Promise.reject(e) + } +} + +module.exports = { ticker } diff --git a/lib/poller.js b/lib/poller.js index 99ea5543..41f1850d 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -33,6 +33,7 @@ const SANCTIONS_UPDATE_INTERVAL = 1 * T.day const RADAR_UPDATE_INTERVAL = 5 * T.minutes const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes +const TICKER_RATES_INTERVAL = 59 * T.seconds const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds const PENDING_INTERVAL = 10 * T.seconds @@ -178,6 +179,7 @@ function doPolling (schema) { notifier.checkNotification(pi()) updateCoinAtmRadar() + addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST) addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST) addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) diff --git a/lib/routes.js b/lib/routes.js index 7e5343ca..e3611152 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -14,6 +14,7 @@ const computeSchema = require('./middlewares/compute-schema') const findOperatorId = require('./middlewares/operatorId') const populateDeviceId = require('./middlewares/populateDeviceId') const populateSettings = require('./middlewares/populateSettings') +const recordPing = require('./middlewares/recordPing') const cashboxRoutes = require('./routes/cashboxRoutes') const customerRoutes = require('./routes/customerRoutes') @@ -29,6 +30,8 @@ const verifyUserRoutes = require('./routes/verifyUserRoutes') const verifyTxRoutes = require('./routes/verifyTxRoutes') const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes') +const graphQLServer = require('./graphql/server') + const app = express() const configRequiredRoutes = [ @@ -38,7 +41,8 @@ const configRequiredRoutes = [ '/phone_code', '/customer', '/tx', - '/verify_promo_code' + '/verify_promo_code', + '/graphql' ] const devMode = argv.dev || process.env.HTTP @@ -55,11 +59,12 @@ app.use('/', pairingRoutes) app.use(findOperatorId) app.use(populateDeviceId) app.use(computeSchema) -if (!devMode) app.use(authorize) +app.use(authorize) app.use(configRequiredRoutes, populateSettings) app.use(filterOldRequests) // other app routes +app.use('/graphql', recordPing) app.use('/poll', pollingRoutes) app.use('/terms_conditions', termsAndConditionsRoutes) app.use('/state', stateRoutes) @@ -78,6 +83,8 @@ app.use('/tx', txRoutes) app.use('/logs', logsRoutes) +graphQLServer.applyMiddleware({ app }) + app.use(errorHandler) app.use((req, res) => { res.status(404).json({ error: 'No such route' }) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index a2ca4847..89e7aab1 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -61,7 +61,6 @@ function poll (req, res, next) { const machineModel = req.query.model const deviceId = req.deviceId const deviceTime = req.deviceTime - const serialNumber = req.query.sn const pid = req.query.pid const settings = req.settings const operatorId = res.locals.operatorId @@ -73,9 +72,6 @@ function poll (req, res, next) { const pi = plugins(settings, deviceId) const hasLightning = checkHasLightning(settings) - const triggersAutomationPromise = configManager.getTriggersAutomation(settings.config) - const triggersPromise = buildTriggers(configManager.getTriggers(settings.config)) - const operatorInfo = configManager.getOperatorInfo(settings.config) const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName } const cashOutConfig = configManager.getCashOut(deviceId, settings.config) @@ -85,10 +81,13 @@ function poll (req, res, next) { state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) - return Promise.all([pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel), triggersPromise, triggersAutomationPromise]) - .then(([results, triggers, triggersAutomation]) => { - const cassettes = results.cassettes - + return Promise.all([ + pi.recordPing(deviceTime, machineVersion, machineModel), + pi.pollQueries(), + buildTriggers(configManager.getTriggers(settings.config)), + configManager.getTriggersAutomation(settings.config) + ]) + .then(([_pingRes, results, triggers, triggersAutomation]) => { const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid const restartServices = pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid @@ -110,7 +109,6 @@ function poll (req, res, next) { receiptPrintingActive: receipt.active, smsReceiptActive: receipt.sms, enablePaperWalletOnly, - cassettes, twoWayMode: cashOutConfig.active, zeroConfLimits, reboot, diff --git a/lib/ticker.js b/lib/ticker.js index c032a079..79ff7737 100644 --- a/lib/ticker.js +++ b/lib/ticker.js @@ -7,7 +7,7 @@ const ccxt = require('./plugins/ticker/ccxt') const mockTicker = require('./plugins/ticker/mock-ticker') const bitpay = require('./plugins/ticker/bitpay') -const FETCH_INTERVAL = 60000 +const FETCH_INTERVAL = 58000 function _getRates (settings, fiatCode, cryptoCode) { return Promise.resolve() diff --git a/new-lamassu-admin/src/components/LogsDownloaderPopper.js b/new-lamassu-admin/src/components/LogsDownloaderPopper.js index 5d879fe7..9c348810 100644 --- a/new-lamassu-admin/src/components/LogsDownloaderPopper.js +++ b/new-lamassu-admin/src/components/LogsDownloaderPopper.js @@ -1,7 +1,7 @@ import { useLazyQuery } from '@apollo/react-hooks' import { makeStyles, ClickAwayListener } from '@material-ui/core' import classnames from 'classnames' -import { format } from 'date-fns/fp' +import { format, set } from 'date-fns/fp' import FileSaver from 'file-saver' import * as R from 'ramda' import React, { useState, useCallback } from 'react' @@ -280,7 +280,15 @@ const LogsDownloaderPopover = ({ )} diff --git a/new-lamassu-admin/src/components/date-range-picker/Calendar.js b/new-lamassu-admin/src/components/date-range-picker/Calendar.js index c6151d73..c0480530 100644 --- a/new-lamassu-admin/src/components/date-range-picker/Calendar.js +++ b/new-lamassu-admin/src/components/date-range-picker/Calendar.js @@ -178,9 +178,7 @@ const Calendar = ({ minDate, maxDate, handleSelect, ...props }) => { {R.range(1, 8).map((row, key) => ( {getRow(currentDisplayedMonth, row).map((day, key) => ( - handleSelect(day, minDate, maxDate)}> + handleSelect(day)}> { const classes = useStyles() - const handleSelect = (day, minDate, maxDate) => { + const handleSelect = day => { if ( - (maxDate && differenceInDays(maxDate, day) > 0) || + (maxDate && compareAsc(maxDate, day) > 0) || (minDate && differenceInDays(day, minDate) > 0) ) return - if (from && !to && differenceInDays(day, from) > 0) { - setTo(from) - setFrom(day) - return - } - - if ( - from && - !to && - (isSameMonth(from, day) || differenceInMonths(from, day) > 0) - ) { - setTo( - set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day) - ) + if (from && !to) { + if (differenceInDays(from, day) >= 0) { + setTo( + set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day) + ) + } else { + setTo(from) + setFrom(day) + } return } diff --git a/package-lock.json b/package-lock.json index 5daa70b8..f5c222af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lamassu-server", - "version": "8.0.0-beta.4", + "version": "8.1.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 539bdfaf..b4771d8a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "lamassu-server", "description": "bitcoin atm client server protocol module", "keywords": [], - "version": "8.0.0-beta.4", + "version": "8.1.0-beta.0", "license": "Unlicense", "author": "Lamassu (https://lamassu.is)", "dependencies": {