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