diff --git a/bin/convert-txs.js b/bin/convert-txs.js
index 71c81f95..658381ef 100755
--- a/bin/convert-txs.js
+++ b/bin/convert-txs.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
var pgp = require('pg-promise')()
diff --git a/bin/lamassu-configure-frontcamera b/bin/lamassu-configure-frontcamera
index 31780f3e..910db546 100755
--- a/bin/lamassu-configure-frontcamera
+++ b/bin/lamassu-configure-frontcamera
@@ -3,7 +3,7 @@
'use strict'
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const setEnvVariable = require('../tools/set-env-var')
diff --git a/bin/lamassu-eth-recovery b/bin/lamassu-eth-recovery
index 1d9c71cd..aeedde67 100644
--- a/bin/lamassu-eth-recovery
+++ b/bin/lamassu-eth-recovery
@@ -1,7 +1,7 @@
#!/usr/bin/env node
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const hdkey = require('ethereumjs-wallet/hdkey')
const hkdf = require('futoin-hkdf')
const db = require('../lib/db')
diff --git a/bin/lamassu-migrate b/bin/lamassu-migrate
index 6650ea91..5e62218a 100755
--- a/bin/lamassu-migrate
+++ b/bin/lamassu-migrate
@@ -3,6 +3,10 @@ const _ = require('lodash/fp')
const path = require('path')
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+const _ = require('lodash/fp')
+const path = require('path')
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
+
const db = require('../lib/db')
const migrate = require('../lib/migrate')
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
diff --git a/bin/lamassu-mnemonic b/bin/lamassu-mnemonic
index fa841b08..3e655ef2 100755
--- a/bin/lamassu-mnemonic
+++ b/bin/lamassu-mnemonic
@@ -2,7 +2,7 @@
const fs = require('fs')
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const MNEMONIC_PATH = process.env.MNEMONIC_PATH
diff --git a/bin/lamassu-ofac-update-sources b/bin/lamassu-ofac-update-sources
index 0a7f9528..786fd35c 100755
--- a/bin/lamassu-ofac-update-sources
+++ b/bin/lamassu-ofac-update-sources
@@ -5,7 +5,7 @@
const setEnvVariable = require('../tools/set-env-var')
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
if (!process.env.OFAC_SOURCES_NAMES && !process.env.OFAC_SOURCES_URLS) {
setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced')
diff --git a/bin/lamassu-operator b/bin/lamassu-operator
index 8249ee64..bedf170c 100644
--- a/bin/lamassu-operator
+++ b/bin/lamassu-operator
@@ -4,7 +4,7 @@ const fs = require('fs')
const hkdf = require('futoin-hkdf')
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const mnemonicHelpers = require('../lib/mnemonic-helpers')
diff --git a/bin/lamassu-register b/bin/lamassu-register
index e2bae59c..542f9ef8 100755
--- a/bin/lamassu-register
+++ b/bin/lamassu-register
@@ -1,14 +1,14 @@
#!/usr/bin/env node
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
const userManagement = require('../lib/new-admin/graphql/modules/userManagement')
const authErrors = require('../lib/new-admin/graphql/errors/authentication')
const name = process.argv[2]
const role = process.argv[3]
-const domain = process.env.LAMASSU_ADMIN_SERVER_IP || process.env.HOSTNAME
+const domain = process.env.HOSTNAME
if (!domain) {
console.error('No hostname configured in the environment')
diff --git a/bin/lamassu-update-to-mnemonic b/bin/lamassu-update-to-mnemonic
index ed14222e..3da8cfe3 100755
--- a/bin/lamassu-update-to-mnemonic
+++ b/bin/lamassu-update-to-mnemonic
@@ -9,7 +9,7 @@ const mnemonicHelpers = require('../lib/mnemonic-helpers')
const setEnvVariable = require('../tools/set-env-var')
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
if (!process.env.MNEMONIC_PATH && process.env.SEED_PATH) {
const seed = fs.readFileSync(process.env.SEED_PATH, 'utf8').trim()
diff --git a/bin/migrate-config.js b/bin/migrate-config.js
index 759e5e31..5d486ef8 100644
--- a/bin/migrate-config.js
+++ b/bin/migrate-config.js
@@ -5,7 +5,7 @@
const pgp = require('pg-promise')()
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const { PSQL_URL } = require('../lib/constants')
diff --git a/bin/old-lamassu-register b/bin/old-lamassu-register
index e6e2313a..3da5a9f4 100755
--- a/bin/old-lamassu-register
+++ b/bin/old-lamassu-register
@@ -1,7 +1,7 @@
#!/usr/bin/env node
const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const login = require('../lib/admin/login')
diff --git a/dev/recreate-seeds.js b/dev/recreate-seeds.js
index b4027093..bd542df1 100644
--- a/dev/recreate-seeds.js
+++ b/dev/recreate-seeds.js
@@ -8,7 +8,7 @@ const os = require('os')
const bip39 = require('bip39')
const setEnvVariable = require('../tools/set-env-var')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
if (process.env.MNEMONIC_PATH && !process.env.SEED_PATH) {
const mnemonic = fs.readFileSync(process.env.MNEMONIC_PATH, 'utf8')
diff --git a/lib/app.js b/lib/app.js
index 449da86a..07e90abd 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -4,7 +4,7 @@ const http = require('http')
const https = require('https')
const argv = require('minimist')(process.argv.slice(2))
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const { asyncLocalStorage, defaultStore } = require('./async-storage')
const routes = require('./routes')
@@ -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/blockchain/bitcoin.js b/lib/blockchain/bitcoin.js
index d68cc13a..103ebd79 100644
--- a/lib/blockchain/bitcoin.js
+++ b/lib/blockchain/bitcoin.js
@@ -31,14 +31,14 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info(`changetype already defined, skipping...`)
} else {
common.logger.info(`Enabling bech32 change addresses in config file..`)
- common.es(`echo -e "\nchangetype=bech32" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
+ common.es(`echo "\nchangetype=bech32" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
}
if (common.es(`grep "listenonion=" /mnt/blockchains/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`listenonion already defined, skipping...`)
} else {
common.logger.info(`Setting 'listenonion=0' in config file...`)
- common.es(`echo -e "\nlistenonion=0" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
+ common.es(`echo "\nlistenonion=0" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
}
if (isCurrentlyRunning) {
@@ -63,5 +63,6 @@ changetype=bech32
walletrbf=1
bind=0.0.0.0:8332
rpcport=8333
-listenonion=0`
+listenonion=0
+`
}
diff --git a/lib/blockchain/bitcoincash.js b/lib/blockchain/bitcoincash.js
index 37dac4f3..ef5a2995 100644
--- a/lib/blockchain/bitcoincash.js
+++ b/lib/blockchain/bitcoincash.js
@@ -46,5 +46,6 @@ keypool=10000
prune=4000
daemon=0
bind=0.0.0.0:8335
-rpcport=8336`
+rpcport=8336
+`
}
diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js
index 4bcd7a93..ea28c639 100644
--- a/lib/blockchain/common.js
+++ b/lib/blockchain/common.js
@@ -23,18 +23,16 @@ module.exports = {
const BINARIES = {
BTC: {
- defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
- defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-22.0/bin'
},
ETH: {
- url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.15-8be800ff.tar.gz',
- dir: 'geth-linux-amd64-1.10.15-8be800ff'
+ url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.17-25c9b49f.tar.gz',
+ dir: 'geth-linux-amd64-1.10.17-25c9b49f'
},
ZEC: {
- url: 'https://z.cash/downloads/zcash-4.6.0-1-linux64-debian-stretch.tar.gz',
- dir: 'zcash-4.6.0-1/bin'
+ url: 'https://z.cash/downloads/zcash-4.6.0-2-linux64-debian-bullseye.tar.gz',
+ dir: 'zcash-4.6.0-2/bin'
},
DASH: {
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
@@ -56,7 +54,7 @@ const BINARIES = {
}
}
-const coinsUpdateDependent = ['BTC']
+const coinsUpdateDependent = []
function firewall (ports) {
if (!ports || ports.length === 0) throw new Error('No ports supplied')
diff --git a/lib/blockchain/dash.js b/lib/blockchain/dash.js
index c8154e7d..05ace3a7 100644
--- a/lib/blockchain/dash.js
+++ b/lib/blockchain/dash.js
@@ -34,7 +34,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info(`enablecoinjoin already defined, skipping...`)
} else {
common.logger.info(`Enabling CoinJoin in config file...`)
- common.es(`echo -e "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`)
+ common.es(`echo "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`)
}
if (common.es(`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`)) {
@@ -44,14 +44,14 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info(`coinjoinautostart already defined, skipping...`)
} else {
common.logger.info(`Enabling CoinJoin AutoStart in config file...`)
- common.es(`echo -e "\ncoinjoinautostart=1" >> /mnt/blockchains/dash/dash.conf`)
+ common.es(`echo "\ncoinjoinautostart=1" >> /mnt/blockchains/dash/dash.conf`)
}
if (common.es(`grep "litemode=" /mnt/blockchains/dash/dash.conf || true`)) {
common.logger.info(`Switching from 'LiteMode' to 'DisableGovernance'...`)
common.es(`sed -i 's/litemode/disablegovernance/g' /mnt/blockchains/dash/dash.conf`)
} else {
- common.es(`echo -e "\ndisablegovernance already defined, skipping..."`)
+ common.es(`echo "\ndisablegovernance already defined, skipping..."`)
}
if (isCurrentlyRunning) {
@@ -71,5 +71,6 @@ disablegovernance=1
prune=4000
txindex=0
enablecoinjoin=1
-coinjoinautostart=1`
+coinjoinautostart=1
+`
}
diff --git a/lib/blockchain/litecoin.js b/lib/blockchain/litecoin.js
index 8a667c23..0bdc2540 100644
--- a/lib/blockchain/litecoin.js
+++ b/lib/blockchain/litecoin.js
@@ -31,7 +31,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info(`changetype already defined, skipping...`)
} else {
common.logger.info(`Enabling bech32 change addresses in config file..`)
- common.es(`echo -e "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`)
+ common.es(`echo "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`)
}
if (isCurrentlyRunning) {
@@ -52,5 +52,6 @@ keypool=10000
prune=4000
daemon=0
addresstype=p2sh-segwit
-changetype=bech32`
+changetype=bech32
+`
}
diff --git a/lib/blockchain/zcash.js b/lib/blockchain/zcash.js
index 52dfd496..777a67b4 100644
--- a/lib/blockchain/zcash.js
+++ b/lib/blockchain/zcash.js
@@ -49,5 +49,6 @@ addnode=mainnet.z.cash
rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
dbcache=500
-keypool=10000`
+keypool=10000
+`
}
diff --git a/lib/cash-out/cash-out-low.js b/lib/cash-out/cash-out-low.js
index 210270af..91130119 100644
--- a/lib/cash-out/cash-out-low.js
+++ b/lib/cash-out/cash-out-low.js
@@ -2,13 +2,14 @@ const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const helper = require('./cash-out-helper')
+const { anonymousCustomer } = require('../constants')
const toDb = helper.toDb
const toObj = helper.toObj
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
- 'receivedCryptoAtoms', 'walletScore' ]
+ 'receivedCryptoAtoms', 'walletScore', 'customerId' ]
module.exports = {upsert, update, insert}
@@ -52,7 +53,15 @@ function diff (oldTx, newTx) {
// We never null out an existing field
if (oldTx && _.isNil(newTx[fieldKey])) return
- updatedTx[fieldKey] = newTx[fieldKey]
+ switch (fieldKey) {
+ case 'customerId':
+ if (oldTx.customerId === anonymousCustomer.uuid) {
+ return updatedTx['customerId'] = newTx['customerId']
+ }
+ return
+ default:
+ return updatedTx[fieldKey] = newTx[fieldKey]
+ }
})
return updatedTx
diff --git a/lib/cashbox-batches.js b/lib/cashbox-batches.js
index e180fa37..a23ec25f 100644
--- a/lib/cashbox-batches.js
+++ b/lib/cashbox-batches.js
@@ -72,22 +72,25 @@ function getBillsByBatchId (id) {
function logFormatter (data) {
return _.map(
it => {
+ const bills = _.filter(
+ ite => !(_.isNil(ite) || _.isNil(ite.fiat_code) || _.isNil(ite.fiat) || _.isNaN(ite.fiat)),
+ it.bills
+ )
return {
id: it.id,
deviceId: it.deviceId,
created: it.created,
operationType: it.operationType,
- performedBy: it.performedBy,
- billCount: _.size(it.bills),
+ billCount: _.size(bills),
fiatTotals: _.reduce(
(acc, value) => {
acc[value.fiat_code] = (acc[value.fiat_code] || 0) + value.fiat
return acc
},
{},
- it.bills
+ bills
),
- billsByDenomination: _.countBy(ite => `${ite.fiat} ${ite.fiat_code}`, it.bills)
+ billsByDenomination: _.countBy(ite => `${ite.fiat} ${ite.fiat_code}`, bills)
}
},
data
diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js
new file mode 100644
index 00000000..ee1538f4
--- /dev/null
+++ b/lib/graphql/resolvers.js
@@ -0,0 +1,276 @@
+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 { batchGetCustomInfoRequest, getCustomInfoRequests } = 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')),
+ batchGetCustomInfoRequest
+ )(customTriggers)
+ .then(res => {
+ res.forEach((details, index) => {
+ // make sure we aren't attaching the details to the wrong trigger
+ if (customTriggers[index].customInfoRequestId !== details.id) return
+ customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
+ })
+ return [...normalTriggers, ...customTriggers]
+ })
+}
+
+const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => {
+ const massageCoins = _.map(_.pick([
+ 'batchable',
+ 'cashInCommission',
+ 'cashInFee',
+ 'cashOutCommission',
+ '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(getCustomInfoRequests(true), 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),
+ !!configManager.getCashOut(deviceId, settings.config).active,
+ ])
+ .then(([
+ enablePaperWalletOnly,
+ triggersAutomation,
+ triggers,
+ hasLightning,
+ localeInfo,
+ operatorInfo,
+ receiptInfo,
+ 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,
+ }),
+ _.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,
+ }),
+ }))
+
+
+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
+
+/*
+ * The type of the result of `configManager.getTermsConditions()` is more or
+ * less `Maybe (Maybe Hash, Maybe TC)`. Each case has a specific meaning to the
+ * machine:
+ *
+ * Nothing => Nothing
+ * There are no T&C or they've been removed/disabled.
+ *
+ * Just (Nothing, _) => Nothing
+ * Shouldn't happen! Treated as if there were no T&C.
+ *
+ * Just (Just hash, Nothing) => Nothing
+ * May happen (after `massageTerms`) if T&C are disabled.
+ *
+ * Just (Just hash, Just tc) => Just (hash, Just tc) or Just (hash, Nothing)
+ * `tc` is sent depending on whether the `hash` differs from `currentHash` or
+ * not.
+ */
+const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settings }, info) => {
+ const isNone = x => _.isNil(x) || _.isEmpty(x)
+
+ let latestTerms = configManager.getTermsConditions(settings.config)
+ if (isNone(latestTerms)) return null
+
+ const hash = latestTerms.hash
+ if (!_.isString(hash)) return null
+
+ latestTerms = massageTerms(latestTerms)
+ if (isNone(latestTerms)) return null
+
+ const isHashNew = hash !== currentHash
+ const text = isHashNew ? latestTerms.text : null
+
+ return plugins(settings, deviceId)
+ .fetchCurrentConfigVersion()
+ .catch(() => null)
+ .then(configVersion => isHashNew || _.isNil(currentConfigVersion) || currentConfigVersion < configVersion)
+ .then(isVersionNew => isVersionNew ? _.omit(['text'], latestTerms) : null)
+ .then(details => ({ hash, details, text }))
+}
+
+
+module.exports = {
+ Query: {
+ configs,
+ terms,
+ }
+}
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..04e2e69f
--- /dev/null
+++ b/lib/graphql/types.js
@@ -0,0 +1,155 @@
+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 TermsDetails {
+ delay: Boolean!
+ title: String!
+ accept: String!
+ cancel: String!
+}
+
+type Terms {
+ hash: String!
+ text: String
+ details: TermsDetails
+}
+
+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!]!
+}
+
+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!
+ terms(currentHash: String, currentConfigVersion: Int): Terms
+}
+`
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/populateDeviceId.js b/lib/middlewares/populateDeviceId.js
index 3bf50761..4c8913ae 100644
--- a/lib/middlewares/populateDeviceId.js
+++ b/lib/middlewares/populateDeviceId.js
@@ -11,7 +11,6 @@ function sha256 (buf) {
}
const populateDeviceId = function (req, res, next) {
- logger.info(`DEBUG LOG - Method: ${req.method} Path: ${req.path}`)
const deviceId = _.isFunction(req.connection.getPeerCertificate)
? sha256(req.connection.getPeerCertificate().raw)
: null
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/migrate-options.js b/lib/migrate-options.js
index 921b8b1b..8c79fa51 100644
--- a/lib/migrate-options.js
+++ b/lib/migrate-options.js
@@ -1,7 +1,9 @@
const _ = require('lodash/fp')
const fs = require('fs')
+const os = require('os')
const makeDir = require('make-dir')
const path = require('path')
+const cp = require('child_process')
const load = require('./options-loader')
const logger = require('./logger')
@@ -53,61 +55,13 @@ function updateOptionBasepath (result, optionName) {
}
async function run () {
- // load defaults
- const defaultOpts = require('../lamassu-default')
-
// load current opts
- const options = load()
- const currentOpts = options.opts
+ const options = load().opts
+ const shouldMigrate = !fs.existsSync(process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env'))
- // check if there are new options to add
- let result = _.mergeAll([defaultOpts, currentOpts])
-
- // get all the options
- // that ends with "Path" suffix
- logger.info(`Detected lamassu-server basepath: ${currentBasePath}`)
- _.each(_.wrap(updateOptionBasepath, result),
- [
- 'seedPath',
- 'caPath',
- 'certPath',
- 'keyPath',
- 'lamassuCaPath'
- ])
-
- const shouldMigrate = !_.isEqual(result, currentOpts) || _.has('lamassuServerPath', result)
-
- // write the resulting lamassu.json
+ // write the resulting .env
if (shouldMigrate) {
- // remove old lamassuServerPath config
- result = _.omit('lamassuServerPath', result)
-
- // find keys for which values
- // have been changed
- const differentValue = _.wrap(_.filter, key => !_.isEqual(result[key], currentOpts[key]))
-
- // output affected options
- const newOpts = _.pick(_.union(
- // find change keys
- differentValue(_.keys(result)),
- // find new opts
- _.difference(_.keys(result), _.keys(currentOpts))
- ), result)
- logger.info('Updating options', newOpts)
-
- // store new lamassu.json file
- fs.writeFileSync(options.path, JSON.stringify(result, null, ' '))
+ const postgresPw = new RegExp(':(\\w*)@').exec(options.postgresql)[1]
+ cp.spawnSync('node', ['tools/build-prod-env.js', '--db-password', postgresPw, '--hostname', options.hostname], { cwd: currentBasePath, encoding: 'utf-8' })
}
-
- // get all the new options
- // that ends with "Dir" suffix
- mapKeyValuesDeep((v, k) => {
- if (_.endsWith('Dir', k)) {
- const path = _.attempt(() => makeDir.sync(v))
-
- if (_.isError(path)) {
- logger.error(`while creating folder ${v}`, path)
- }
- }
- }, result)
}
diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js
index b6ec0221..7d633c08 100644
--- a/lib/new-admin/admin-server.js
+++ b/lib/new-admin/admin-server.js
@@ -12,7 +12,7 @@ const { graphqlUploadExpress } = require('graphql-upload')
const { ApolloServer } = require('apollo-server-express')
const _ = require('lodash/fp')
-require('dotenv').config({ path: path.resolve(__dirname, '../../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const { asyncLocalStorage, defaultStore } = require('../async-storage')
const logger = require('../logger')
diff --git a/lib/new-admin/graphql-dev-insecure.js b/lib/new-admin/graphql-dev-insecure.js
index 8d673d54..c8468208 100644
--- a/lib/new-admin/graphql-dev-insecure.js
+++ b/lib/new-admin/graphql-dev-insecure.js
@@ -2,7 +2,7 @@ const express = require('express')
const path = require('path')
const { ApolloServer } = require('apollo-server-express')
-require('dotenv').config({ path: path.resolve(__dirname, '../../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const { typeDefs, resolvers } = require('./graphql/schema')
const logger = require('../logger')
diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js
index 4c05b540..58638ebc 100644
--- a/lib/new-config-manager.js
+++ b/lib/new-config-manager.js
@@ -1,5 +1,4 @@
const _ = require('lodash/fp')
-const { getCustomInfoRequests } = require('./new-admin/services/customInfoRequests')
const namespaces = {
ADVANCED: 'advanced',
@@ -21,6 +20,7 @@ const filter = namespace => _.pickBy((value, key) => _.startsWith(`${namespace}_
const strip = key => _.mapKeys(stripl(`${key}_`))
const fromNamespace = _.curry((key, config) => _.compose(strip(key), filter(key))(config))
+const toNamespace = _.curry((ns, config) => _.mapKeys(key => `${ns}_${key}`, config))
const getCommissions = (cryptoCode, deviceId, config) => {
const commissions = fromNamespace(namespaces.COMMISSIONS)(config)
@@ -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)]))
}
@@ -117,8 +116,9 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
const getTriggers = _.get('triggers')
-const getTriggersAutomation = config => {
- return getCustomInfoRequests(true)
+/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
+const getTriggersAutomation = (customInfoRequests, config) => {
+ return customInfoRequests
.then(infoRequests => {
const defaultAutomation = _.get('triggersConfig_automation')(config)
const requirements = {
@@ -155,6 +155,8 @@ const getCryptoUnits = (crypto, config) => {
return getWalletSettings(crypto, config).cryptoUnits
}
+const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS)
+
module.exports = {
getWalletSettings,
getCashInSettings,
@@ -174,5 +176,6 @@ module.exports = {
getGlobalCashOut,
getCashOut,
getCryptosFromWalletNamespace,
- getCryptoUnits
+ getCryptoUnits,
+ setTermsConditions,
}
diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js
index 2f812c51..a9af0f30 100644
--- a/lib/new-settings-loader.js
+++ b/lib/new-settings-loader.js
@@ -1,8 +1,11 @@
+const crypto = require('crypto')
+
const _ = require('lodash/fp')
const db = require('./db')
const migration = require('./config-migration')
const { asyncLocalStorage } = require('./async-storage')
const { getOperatorId } = require('./operator')
+const { getTermsConditions, setTermsConditions } = require('./new-config-manager')
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2
@@ -23,6 +26,29 @@ const SECRET_FIELDS = [
'twilio.authToken'
]
+/*
+ * JSON.stringify isn't necessarily deterministic so this function may compute
+ * different hashes for the same object.
+ */
+const md5hash = text =>
+ crypto
+ .createHash('MD5')
+ .update(text)
+ .digest('hex')
+
+const addTermsHash = configs => {
+ const terms = _.omit(['hash'], getTermsConditions(configs))
+ return _.isEmpty(terms) ?
+ configs :
+ _.flow(
+ _.get('text'),
+ md5hash,
+ hash => _.set('hash', hash, terms),
+ setTermsConditions,
+ _.assign(configs),
+ )(terms)
+}
+
const accountsSql = `update user_config set data = $2, valid = $3, schema_version = $4 where type = $1;
insert into user_config (type, data, valid, schema_version)
select $1, $2, $3, $4 where $1 not in (select type from user_config)`
@@ -74,7 +100,7 @@ const configSql = 'insert into user_config (type, data, valid, schema_version) v
function saveConfig (config) {
return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')])
.then(([currentConfig, operatorId]) => {
- const newConfig = _.assign(currentConfig, config)
+ const newConfig = addTermsHash(_.assign(currentConfig, config))
return db.tx(t => {
return t.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(() => t.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]))
diff --git a/lib/options-loader.js b/lib/options-loader.js
index 4f05ee5a..5c81fd66 100644
--- a/lib/options-loader.js
+++ b/lib/options-loader.js
@@ -4,7 +4,7 @@ const os = require('os')
const argv = require('minimist')(process.argv.slice(2))
const _ = require('lodash/fp')
-require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
+require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') })
const DATABASE = process.env.LAMASSU_DB ?? 'PROD'
const dbMapping = psqlConf => ({
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/common/json-rpc.js b/lib/plugins/common/json-rpc.js
index 2a93a2f7..be82ac0b 100644
--- a/lib/plugins/common/json-rpc.js
+++ b/lib/plugins/common/json-rpc.js
@@ -39,11 +39,11 @@ function fetch (account = {}, method, params) {
return r.data.result
})
.catch(err => {
- throw new Error(_.join(' ', [
- 'json-rpc::axios error:',
- JSON.stringify(_.get('message', err, '')),
- JSON.stringify(_.get('response.data.error', err, ''))
- ]))
+ throw new Error(JSON.stringify({
+ responseMessage: _.get('message', err),
+ message: _.get('response.data.error.message', err),
+ code: _.get('response.data.error.code', err)
+ }))
})
}
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/plugins/wallet/bitcoincashd/bitcoincashd.js b/lib/plugins/wallet/bitcoincashd/bitcoincashd.js
index 857da1d0..18a5c96a 100644
--- a/lib/plugins/wallet/bitcoincashd/bitcoincashd.js
+++ b/lib/plugins/wallet/bitcoincashd/bitcoincashd.js
@@ -14,6 +14,16 @@ function fetch (method, params) {
return jsonRpc.fetch(rpcConfig, method, params)
}
+function errorHandle (e) {
+ const err = JSON.parse(e.message)
+ switch (err.code) {
+ case -6:
+ throw new E.InsufficientFundsError()
+ default:
+ throw e
+ }
+}
+
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'BCH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
return Promise.resolve()
@@ -50,10 +60,7 @@ function sendCoins (account, tx, settings, operatorId) {
txid: pickedObj.txid
}
})
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function newAddress (account, info, tx, settings, operatorId) {
diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js
index 24c24381..66fb6ed6 100644
--- a/lib/plugins/wallet/bitcoind/bitcoind.js
+++ b/lib/plugins/wallet/bitcoind/bitcoind.js
@@ -17,21 +17,55 @@ function fetch (method, params) {
return jsonRpc.fetch(rpcConfig, method, params)
}
+function errorHandle (e) {
+ const err = JSON.parse(e.message)
+ switch (err.code) {
+ case -4:
+ return loadWallet()
+ case -5:
+ return logger.error(`${err}`)
+ case -6:
+ throw new E.InsufficientFundsError()
+ case -18:
+ return createWallet()
+ case -35:
+ // Wallet is already loaded, just return
+ return
+ default:
+ throw e
+ }
+}
+
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'BTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
- return Promise.resolve()
+ return Promise.resolve().then(loadWallet)
+}
+
+function createWallet () {
+ return fetch('createwallet', ['wallet'])
+ .then(loadWallet)
+}
+
+function loadWallet () {
+ return fetch('loadwallet', ['wallet', true])
+ // Catching the error here to suppress error code -35
+ // This improves UX on the initial wallet load and serves as error sink
+ // for wallet creation/loading related issues before actual business logic runs
+ .catch(errorHandle)
}
function accountBalance (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getwalletinfo'))
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
+ .catch(errorHandle)
}
function accountUnconfirmedBalance (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getwalletinfo'))
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
+ .catch(errorHandle)
}
// We want a balance that includes all spends (0 conf) but only deposits that
@@ -75,10 +109,7 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
txid: pickedObj.txid
}
})
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
@@ -98,20 +129,19 @@ function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
txid: pickedObj.txid
}))
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function newAddress (account, info, tx, settings, operatorId) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
+ .catch(errorHandle)
}
function addressBalance (address, confs) {
return fetch('getreceivedbyaddress', [address, confs])
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
+ .catch(errorHandle)
}
function confirmedBalance (address, cryptoCode) {
@@ -156,6 +186,7 @@ function newFunding (account, cryptoCode, settings, operatorId) {
fundingConfirmedBalance,
fundingAddress
}))
+ .catch(errorHandle)
}
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
@@ -169,7 +200,7 @@ function fetchRBF (txId) {
return [txId, res['bip125-replaceable']]
})
.catch(err => {
- if (err.code === -5) logger.error(`${err.message}`)
+ errorHandle(err)
return [txId, true]
})
}
diff --git a/lib/plugins/wallet/dashd/dashd.js b/lib/plugins/wallet/dashd/dashd.js
index 4d759e0d..57d3ccc8 100644
--- a/lib/plugins/wallet/dashd/dashd.js
+++ b/lib/plugins/wallet/dashd/dashd.js
@@ -15,6 +15,16 @@ function fetch (method, params) {
return jsonRpc.fetch(rpcConfig, method, params)
}
+function errorHandle (e) {
+ const err = JSON.parse(e.message)
+ switch (err.code) {
+ case -6:
+ throw new E.InsufficientFundsError()
+ default:
+ throw e
+ }
+}
+
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'DASH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
return Promise.resolve()
@@ -52,10 +62,7 @@ function sendCoins (account, tx, settings, operatorId) {
txid: pickedObj.txid
}
})
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function newAddress (account, info, tx, settings, operatorId) {
diff --git a/lib/plugins/wallet/litecoind/litecoind.js b/lib/plugins/wallet/litecoind/litecoind.js
index ab25d626..2168dde8 100644
--- a/lib/plugins/wallet/litecoind/litecoind.js
+++ b/lib/plugins/wallet/litecoind/litecoind.js
@@ -15,6 +15,16 @@ function fetch (method, params) {
return jsonRpc.fetch(rpcConfig, method, params)
}
+function errorHandle (e) {
+ const err = JSON.parse(e.message)
+ switch (err.code) {
+ case -6:
+ throw new E.InsufficientFundsError()
+ default:
+ throw e
+ }
+}
+
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'LTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
return Promise.resolve()
@@ -52,10 +62,7 @@ function sendCoins (account, tx, settings, operatorId) {
txid: pickedObj.txid
}
})
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function newAddress (account, info, tx, settings, operatorId) {
diff --git a/lib/plugins/wallet/zcashd/zcashd.js b/lib/plugins/wallet/zcashd/zcashd.js
index 449c4436..b55d1ec5 100644
--- a/lib/plugins/wallet/zcashd/zcashd.js
+++ b/lib/plugins/wallet/zcashd/zcashd.js
@@ -16,6 +16,16 @@ function fetch (method, params) {
return jsonRpc.fetch(rpcConfig, method, params)
}
+function errorHandle (e) {
+ const err = JSON.parse(e.message)
+ switch (err.code) {
+ case -6:
+ throw new E.InsufficientFundsError()
+ default:
+ throw e
+ }
+}
+
function checkCryptoCode (cryptoCode) {
if (cryptoCode !== 'ZEC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
return Promise.resolve()
@@ -78,10 +88,7 @@ function sendCoins (account, tx, settings, operatorId) {
txid: pickedObj.txid
}
})
- .catch(err => {
- if (err.code === -6) throw new E.InsufficientFundsError()
- throw err
- })
+ .catch(errorHandle)
}
function newAddress (account, info, tx, settings, operatorId) {
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..e0f4b4c2 100644
--- a/lib/routes/pollingRoutes.js
+++ b/lib/routes/pollingRoutes.js
@@ -10,7 +10,7 @@ const plugins = require('../plugins')
const semver = require('semver')
const state = require('../middlewares/state')
const version = require('../../package.json').version
-const customRequestQueries = require('../new-admin/services/customInfoRequests')
+const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
const urlsToPing = [
`us.archive.ubuntu.com`,
@@ -45,7 +45,7 @@ const buildTriggers = (allTriggers) => {
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
}, allTriggers)
- return _.flow([_.map(_.get('customInfoRequestId')), customRequestQueries.batchGetCustomInfoRequest])(customTriggers)
+ return _.flow([_.map(_.get('customInfoRequestId')), batchGetCustomInfoRequest])(customTriggers)
.then(res => {
res.forEach((details, index) => {
// make sure we aren't attaching the details to the wrong trigger
@@ -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(getCustomInfoRequests(true), 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/migrations/1649944954805-terms-and-conditions-hash.js b/migrations/1649944954805-terms-and-conditions-hash.js
new file mode 100644
index 00000000..3b905e22
--- /dev/null
+++ b/migrations/1649944954805-terms-and-conditions-hash.js
@@ -0,0 +1,11 @@
+const { saveConfig } = require('../lib/new-settings-loader')
+
+exports.up = function (next) {
+ return saveConfig({})
+ .then(next)
+ .catch(next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/new-lamassu-admin/.env b/new-lamassu-admin/.env
index 074e97a8..81016f7c 100644
--- a/new-lamassu-admin/.env
+++ b/new-lamassu-admin/.env
@@ -2,4 +2,4 @@ SKIP_PREFLIGHT_CHECK=true
HTTPS=true
REACT_APP_TYPE_CHECK_SANCTUARY=false
PORT=3001
-REACT_APP_BUILD_TARGET=LAMASSU
\ No newline at end of file
+REACT_APP_BUILD_TARGET=PAZUZ
\ No newline at end of file
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 = ({
)}