Merge branch 'dev' into feat-decouple-l-s-entrypoint
This commit is contained in:
commit
9f13873e36
18 changed files with 569 additions and 136 deletions
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
239
lib/graphql/resolvers.js
Normal file
239
lib/graphql/resolvers.js
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
27
lib/graphql/server.js
Normal file
27
lib/graphql/server.js
Normal file
|
|
@ -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
|
||||
})
|
||||
151
lib/graphql/types.js
Normal file
151
lib/graphql/types.js
Normal file
|
|
@ -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!
|
||||
}
|
||||
`
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
7
lib/middlewares/recordPing.js
Normal file
7
lib/middlewares/recordPing.js
Normal file
|
|
@ -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())
|
||||
|
|
@ -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)]))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
)}
|
||||
</div>
|
||||
<DateRangePicker
|
||||
maxDate={new Date()}
|
||||
maxDate={set(
|
||||
{
|
||||
hours: 23,
|
||||
minutes: 59,
|
||||
seconds: 59,
|
||||
milliseconds: 999
|
||||
},
|
||||
new Date()
|
||||
)}
|
||||
onRangeChange={handleRangeChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -178,9 +178,7 @@ const Calendar = ({ minDate, maxDate, handleSelect, ...props }) => {
|
|||
{R.range(1, 8).map((row, key) => (
|
||||
<tr key={key}>
|
||||
{getRow(currentDisplayedMonth, row).map((day, key) => (
|
||||
<td
|
||||
key={key}
|
||||
onClick={() => handleSelect(day, minDate, maxDate)}>
|
||||
<td key={key} onClick={() => handleSelect(day)}>
|
||||
<Tile
|
||||
isDisabled={
|
||||
(maxDate && isAfter(maxDate, day)) ||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
differenceInDays,
|
||||
differenceInMonths,
|
||||
isSameMonth,
|
||||
set
|
||||
} from 'date-fns/fp'
|
||||
import { compareAsc, differenceInDays, set } from 'date-fns/fp'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import Calendar from './Calendar'
|
||||
|
|
@ -29,27 +24,22 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "lamassu-server",
|
||||
"version": "8.0.0-beta.4",
|
||||
"version": "8.1.0-beta.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue