From e609f755c7bb16ed1df9778b0ea17f3f705f76b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Thu, 24 Mar 2022 17:57:01 +0000 Subject: [PATCH 01/44] fix: update db upon empty network metrics --- lib/machine-loader.js | 2 ++ 1 file changed, 2 insertions(+) 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 = { From 00adc25d15241e18db0601b24f0a5c7c79e7e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 31 Mar 2022 22:06:05 +0100 Subject: [PATCH 02/44] fix: add customerId to the list of updatable fields --- lib/cash-out/cash-out-low.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cash-out/cash-out-low.js b/lib/cash-out/cash-out-low.js index 210270af..2772374f 100644 --- a/lib/cash-out/cash-out-low.js +++ b/lib/cash-out/cash-out-low.js @@ -8,7 +8,7 @@ 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} From a5584107963ba976b7a7e3f5dccb2cc41d60f81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 4 Apr 2022 16:35:44 +0100 Subject: [PATCH 03/44] fix: only change customerId in presence of an anonymous customer --- lib/cash-out/cash-out-low.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/cash-out/cash-out-low.js b/lib/cash-out/cash-out-low.js index 2772374f..91130119 100644 --- a/lib/cash-out/cash-out-low.js +++ b/lib/cash-out/cash-out-low.js @@ -2,6 +2,7 @@ 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 @@ -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 From 24807a70ef741b8fe0864147048bc23ed59827ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 4 Apr 2022 17:04:37 +0100 Subject: [PATCH 04/44] fix: cashbox batches log possible NaN and null values --- lib/cashbox-batches.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 From a19d97eb2c63ce6cf4528195592d1ea4c2e07edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Wed, 6 Apr 2022 15:39:54 +0100 Subject: [PATCH 05/44] fix: `sh`'s `echo` doesn't have `-e` --- lib/blockchain/bitcoin.js | 4 ++-- lib/blockchain/dash.js | 6 +++--- lib/blockchain/litecoin.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/blockchain/bitcoin.js b/lib/blockchain/bitcoin.js index d68cc13a..6acc0e93 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) { diff --git a/lib/blockchain/dash.js b/lib/blockchain/dash.js index c8154e7d..f7e18223 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) { diff --git a/lib/blockchain/litecoin.js b/lib/blockchain/litecoin.js index 8a667c23..915d512f 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) { From 5bc45a8f1273faabcec6dee1980af829b0cb5941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Wed, 6 Apr 2022 16:58:08 +0100 Subject: [PATCH 06/44] fix: add `\n` to the last line --- lib/blockchain/bitcoin.js | 3 ++- lib/blockchain/bitcoincash.js | 3 ++- lib/blockchain/dash.js | 3 ++- lib/blockchain/litecoin.js | 3 ++- lib/blockchain/zcash.js | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/blockchain/bitcoin.js b/lib/blockchain/bitcoin.js index 6acc0e93..103ebd79 100644 --- a/lib/blockchain/bitcoin.js +++ b/lib/blockchain/bitcoin.js @@ -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/dash.js b/lib/blockchain/dash.js index f7e18223..05ace3a7 100644 --- a/lib/blockchain/dash.js +++ b/lib/blockchain/dash.js @@ -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 915d512f..0bdc2540 100644 --- a/lib/blockchain/litecoin.js +++ b/lib/blockchain/litecoin.js @@ -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 +` } From e97d9a9ee49d5d5e44c181228ca8052dda1ed487 Mon Sep 17 00:00:00 2001 From: naconner <7396812+naconner@users.noreply.github.com> Date: Sat, 9 Apr 2022 11:04:07 -0400 Subject: [PATCH 07/44] chore: update zcashd & geth --- lib/blockchain/common.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 4bcd7a93..deb068c5 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -29,12 +29,12 @@ const BINARIES = { 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', From 0135d504169a5c3397be658e7ec7a6ec182a9eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Tue, 29 Mar 2022 17:38:52 +0100 Subject: [PATCH 08/44] fix: RPC error handling feat: create and load wallets --- lib/blockchain/common.js | 4 +- lib/plugins/common/json-rpc.js | 10 ++--- .../wallet/bitcoincashd/bitcoincashd.js | 15 +++++-- lib/plugins/wallet/bitcoind/bitcoind.js | 43 +++++++++++++++---- lib/plugins/wallet/dashd/dashd.js | 15 +++++-- lib/plugins/wallet/litecoind/litecoind.js | 15 +++++-- lib/plugins/wallet/zcashd/zcashd.js | 15 +++++-- 7 files changed, 84 insertions(+), 33 deletions(-) diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 4bcd7a93..2904944f 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -23,8 +23,6 @@ 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' }, @@ -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/plugins/common/json-rpc.js b/lib/plugins/common/json-rpc.js index d88823be..50372116 100644 --- a/lib/plugins/common/json-rpc.js +++ b/lib/plugins/common/json-rpc.js @@ -41,11 +41,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/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..4318a8e1 100644 --- a/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/lib/plugins/wallet/bitcoind/bitcoind.js @@ -17,21 +17,49 @@ 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() + default: + throw e + } +} + function checkCryptoCode (cryptoCode) { if (cryptoCode !== 'BTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } +function createWallet () { + return fetch('createwallet', ['wallet']) + .catch(errorHandle) +} + +function loadWallet () { + return fetch('loadwallet', ['wallet', true]) + .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 +103,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 +123,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 +180,7 @@ function newFunding (account, cryptoCode, settings, operatorId) { fundingConfirmedBalance, fundingAddress })) + .catch(console.log) } function cryptoNetwork (account, cryptoCode, settings, operatorId) { @@ -169,7 +194,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) { From 0a591658aa8bdc24ffe1296d25fc59e7f1ea84fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Sun, 3 Apr 2022 23:12:36 +0100 Subject: [PATCH 09/44] fix: remove log --- lib/plugins/wallet/bitcoind/bitcoind.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js index 4318a8e1..fa22b750 100644 --- a/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/lib/plugins/wallet/bitcoind/bitcoind.js @@ -180,7 +180,7 @@ function newFunding (account, cryptoCode, settings, operatorId) { fundingConfirmedBalance, fundingAddress })) - .catch(console.log) + .catch(errorHandle) } function cryptoNetwork (account, cryptoCode, settings, operatorId) { From d854f0580e9c80c32a6bcb6074f79bf107ebfa88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Sun, 10 Apr 2022 17:07:20 +0100 Subject: [PATCH 10/44] fix: improve error handling and funding UX --- lib/plugins/wallet/bitcoind/bitcoind.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js index fa22b750..66fb6ed6 100644 --- a/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/lib/plugins/wallet/bitcoind/bitcoind.js @@ -28,6 +28,9 @@ function errorHandle (e) { throw new E.InsufficientFundsError() case -18: return createWallet() + case -35: + // Wallet is already loaded, just return + return default: throw e } @@ -35,16 +38,19 @@ function errorHandle (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']) - .catch(errorHandle) + .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) } From d4a4f10836bf9a9ae256bd7a90d6947c4631d216 Mon Sep 17 00:00:00 2001 From: naconner <7396812+naconner@users.noreply.github.com> Date: Thu, 14 Apr 2022 08:47:28 -0400 Subject: [PATCH 11/44] chore: version v8.0.0-beta.4 (#1189) --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ac8d3d7..5daa70b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lamassu-server", - "version": "8.0.0-beta.3", + "version": "8.0.0-beta.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1e032d07..539bdfaf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "lamassu-server", "description": "bitcoin atm client server protocol module", "keywords": [], - "version": "8.0.0-beta.3", + "version": "8.0.0-beta.4", "license": "Unlicense", "author": "Lamassu (https://lamassu.is)", "dependencies": { From bc8335adee60f43ed19faeefd9210247a1f97e55 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 14 Apr 2022 19:07:26 +0200 Subject: [PATCH 12/44] fix: date picker --- .../components/date-range-picker/Calendar.js | 4 +-- .../date-range-picker/DateRangePicker.js | 30 +++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/new-lamassu-admin/src/components/date-range-picker/Calendar.js b/new-lamassu-admin/src/components/date-range-picker/Calendar.js index c6151d73..c0480530 100644 --- a/new-lamassu-admin/src/components/date-range-picker/Calendar.js +++ b/new-lamassu-admin/src/components/date-range-picker/Calendar.js @@ -178,9 +178,7 @@ const Calendar = ({ minDate, maxDate, handleSelect, ...props }) => { {R.range(1, 8).map((row, key) => ( {getRow(currentDisplayedMonth, row).map((day, key) => ( - handleSelect(day, minDate, maxDate)}> + handleSelect(day)}> { ) 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 } From fea170e141a312af012799b45598ffed73876b59 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 14 Apr 2022 19:26:23 +0200 Subject: [PATCH 13/44] fix: max date --- .../src/components/LogsDownloaderPopper.js | 12 ++++++++++-- .../components/date-range-picker/DateRangePicker.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/new-lamassu-admin/src/components/LogsDownloaderPopper.js b/new-lamassu-admin/src/components/LogsDownloaderPopper.js index 5d879fe7..9c348810 100644 --- a/new-lamassu-admin/src/components/LogsDownloaderPopper.js +++ b/new-lamassu-admin/src/components/LogsDownloaderPopper.js @@ -1,7 +1,7 @@ import { useLazyQuery } from '@apollo/react-hooks' import { makeStyles, ClickAwayListener } from '@material-ui/core' import classnames from 'classnames' -import { format } from 'date-fns/fp' +import { format, set } from 'date-fns/fp' import FileSaver from 'file-saver' import * as R from 'ramda' import React, { useState, useCallback } from 'react' @@ -280,7 +280,15 @@ const LogsDownloaderPopover = ({ )} diff --git a/new-lamassu-admin/src/components/date-range-picker/DateRangePicker.js b/new-lamassu-admin/src/components/date-range-picker/DateRangePicker.js index b24e6622..7552c5d4 100644 --- a/new-lamassu-admin/src/components/date-range-picker/DateRangePicker.js +++ b/new-lamassu-admin/src/components/date-range-picker/DateRangePicker.js @@ -1,6 +1,6 @@ import { makeStyles } from '@material-ui/core/styles' import classnames from 'classnames' -import { differenceInDays, set } from 'date-fns/fp' +import { compareAsc, differenceInDays, set } from 'date-fns/fp' import React, { useState, useEffect } from 'react' import Calendar from './Calendar' @@ -24,9 +24,9 @@ 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 From 7ddda203647dd4f40237d024772763e987bb11b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 19 Apr 2022 12:43:11 +0100 Subject: [PATCH 14/44] refactor: use Unix fileformat --- lib/plugins/ticker/ccxt.js | 98 +++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 4f74f811..6c495a1c 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -1,49 +1,49 @@ -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 + +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 } From 4747e0b7f69f9fcde29d523d3addd5a3763d5258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 19 Apr 2022 12:43:54 +0100 Subject: [PATCH 15/44] refactor: save CCXT ticker instance --- lib/plugins/ticker/ccxt.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 6c495a1c..09d8dbd7 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -6,8 +6,13 @@ const { getRate } = require('../../../lib/forex') const RETRIES = 2 +const tickerObjects = {} + function ticker (fiatCode, cryptoCode, tickerName) { - const ticker = new ccxt[tickerName]({ timeout: 3000 }) + const ticker = tickerObjects[tickerName] ? + tickerObjects[tickerName] : + tickerObjects[tickerName] = new ccxt[tickerName]({ timeout: 3000 }) + if (verifyFiatSupport(fiatCode, tickerName)) { return getCurrencyRates(ticker, fiatCode, cryptoCode) } From 6c2c6353aa7658bcf4ecf8134f06a35f6558f9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 19 Apr 2022 13:32:47 +0100 Subject: [PATCH 16/44] feat: lower the ticker rate limit --- lib/plugins/ticker/ccxt.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 09d8dbd7..154d7eb4 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -11,7 +11,11 @@ const tickerObjects = {} function ticker (fiatCode, cryptoCode, tickerName) { const ticker = tickerObjects[tickerName] ? tickerObjects[tickerName] : - tickerObjects[tickerName] = new ccxt[tickerName]({ timeout: 3000 }) + tickerObjects[tickerName] = new ccxt[tickerName]({ + timeout: 3000, + enableRateLimit: true, + rateLimit: 333, + }) if (verifyFiatSupport(fiatCode, tickerName)) { return getCurrencyRates(ticker, fiatCode, cryptoCode) From 396e7058f5b29bb7b6857bd22a7a09cf09726c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 19 Apr 2022 14:20:09 +0100 Subject: [PATCH 17/44] refactor: disable CCXT's rate limitting --- lib/plugins/ticker/ccxt.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 154d7eb4..410b24fa 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -13,8 +13,7 @@ function ticker (fiatCode, cryptoCode, tickerName) { tickerObjects[tickerName] : tickerObjects[tickerName] = new ccxt[tickerName]({ timeout: 3000, - enableRateLimit: true, - rateLimit: 333, + enableRateLimit: false, }) if (verifyFiatSupport(fiatCode, tickerName)) { From 2fe92c2ebf5dd9736d833312a0b6082fa572e684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 19 Apr 2022 15:38:13 +0100 Subject: [PATCH 18/44] feat: periodically update ticker rates --- lib/poller.js | 2 ++ lib/ticker.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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/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() From f4d6b5e454fd4788aacbfdfb5401f2ab4845c7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Tue, 29 Mar 2022 15:00:52 +0100 Subject: [PATCH 19/44] feat: decouple l-s entrypoint --- .env | 1 - .sample.env | 59 +++++++++++++++++++ bin/convert-txs.js | 10 +++- bin/lamassu-configure-frontcamera | 10 ++-- bin/lamassu-eth-recovery | 8 ++- bin/lamassu-migrate | 12 +++- bin/lamassu-mnemonic | 6 +- bin/lamassu-ofac-update-sources | 22 ++----- bin/lamassu-operator | 8 ++- bin/lamassu-register | 7 ++- bin/lamassu-update-to-mnemonic | 23 ++++---- bin/migrate-config.js | 8 ++- bin/old-lamassu-register | 8 ++- dev/notify.js | 4 +- dev/recreate-seeds.js | 20 +++---- lib/admin/admin-server.js | 41 ++++++------- lib/admin/admin-support.js | 10 ++-- lib/admin/pairing.js | 7 +-- lib/app.js | 15 +++-- lib/blockchain/do-volume.js | 6 +- lib/blockchain/install.js | 9 +-- lib/coinatmradar/coinatmradar.js | 8 +-- lib/constants.js | 11 +++- lib/customers.js | 16 ++--- lib/db.js | 4 +- lib/logger.js | 8 ++- lib/new-admin/admin-server.js | 31 +++++----- lib/new-admin/graphql-dev-insecure.js | 3 + lib/new-admin/middlewares/session.js | 3 - lib/new-admin/services/pairing.js | 9 +-- lib/ofac/index.js | 9 +-- lib/ofac/update.js | 23 +++++--- lib/options-loader.js | 3 +- lib/pairing.js | 8 +-- lib/plugins/common/json-rpc.js | 6 +- lib/plugins/layer2/strike/strike.js | 4 +- lib/plugins/wallet/lnd/lnd.js | 5 +- lib/plugins/wallet/monerod/monerod.js | 7 +-- .../wallet/pazuz-wallet/pazuz-wallet.js | 13 ++-- lib/routes.js | 3 +- lib/wallet.js | 5 +- .../1623413776161-create-operator-ids.js | 3 +- new-lamassu-admin/.env | 2 +- .../src/pages/Accounting/Accounting.js | 36 ++++++----- tools/build-dev-env.js | 32 ++++++++++ tools/build-prod-env.js | 45 ++++++++++++++ tools/cert-gen.sh | 28 +-------- tools/set-env-var.js | 24 ++++++++ 48 files changed, 411 insertions(+), 232 deletions(-) delete mode 100644 .env create mode 100644 .sample.env create mode 100644 tools/build-dev-env.js create mode 100644 tools/build-prod-env.js create mode 100644 tools/set-env-var.js diff --git a/.env b/.env deleted file mode 100644 index db93e716..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -LAMASSU_DB=DEV \ No newline at end of file diff --git a/.sample.env b/.sample.env new file mode 100644 index 00000000..ae44d16f --- /dev/null +++ b/.sample.env @@ -0,0 +1,59 @@ +## Database variables + +# Used to describe which database to use. Possible values include: DEV, RELEASE, STRESS_TEST +LAMASSU_DB= + +# Postgres related environment variables +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_HOST= +POSTGRES_PORT= +POSTGRES_DB= + +## File paths + +# Certificate-related variables +LAMASSU_CA_PATH= +CA_PATH= +CERT_PATH= +KEY_PATH= + +# Full path to where the wallet's mnemonic is stored +MNEMONIC_PATH= + +MIGRATE_STATE_PATH= + +## Directories + +BLOCKCHAIN_DIR= +OFAC_DATA_DIR= +ID_PHOTO_CARD_DIR= +FRONT_CAMERA_DIR= +OPERATOR_DATA_DIR= + +## URLs + +STRIKE_BASE_URL= +COIN_ATM_RADAR_URL= + +## OFAC Sources variables + +# These variables map to each other, similar to a zip HOF. Entries are separated by commas +# Example: +# OFAC_SOURCES_NAMES=name1,name2 +# OFAC_SOURCES_URLS=url1,url2 +OFAC_SOURCES_NAMES= +OFAC_SOURCES_URLS= + +## Misc + +HOSTNAME= +LOG_LEVEL= +LIGHTNING_NETWORK_DAEMON= + +## Deprecated or in deprecation + +HTTP= +DEV_MODE= + +## Uncategorized variables diff --git a/bin/convert-txs.js b/bin/convert-txs.js index 2e724f98..71c81f95 100755 --- a/bin/convert-txs.js +++ b/bin/convert-txs.js @@ -1,9 +1,13 @@ #!/usr/bin/env node -var pgp = require('pg-promise')() -var psqlUrl = require('../lib/options').postgresql +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) -var db = pgp(psqlUrl) +var pgp = require('pg-promise')() + +const { PSQL_URL } = require('../lib/constants') + +var db = pgp(PSQL_URL) db.manyOrNone(`select * from transactions where incoming=false and stage='final_request' and authority='machine'`) diff --git a/bin/lamassu-configure-frontcamera b/bin/lamassu-configure-frontcamera index 5a6d4d06..31780f3e 100755 --- a/bin/lamassu-configure-frontcamera +++ b/bin/lamassu-configure-frontcamera @@ -2,11 +2,11 @@ 'use strict' -const fs = require('fs') -const options = require('../lib/options-loader')() +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) -if (!options.opts.frontCameraDir) { - options.opts.frontCameraDir = '/opt/lamassu-server/frontcamera' +const setEnvVariable = require('../tools/set-env-var') - fs.writeFileSync(options.path, JSON.stringify(options.opts, null, '\t'), 'utf8') +if (!process.env.FRONT_CAMERA_DIR) { + setEnvVariable('FRONT_CAMERA_DIR', '/opt/lamassu-server/frontcamera') } diff --git a/bin/lamassu-eth-recovery b/bin/lamassu-eth-recovery index e8e2991a..1d9c71cd 100644 --- a/bin/lamassu-eth-recovery +++ b/bin/lamassu-eth-recovery @@ -1,5 +1,7 @@ #!/usr/bin/env node +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) const hdkey = require('ethereumjs-wallet/hdkey') const hkdf = require('futoin-hkdf') const db = require('../lib/db') @@ -9,14 +11,14 @@ const mnemonicHelpers = require('../lib/mnemonic-helpers') const pify = require('pify') const fs = pify(require('fs')) -const options = require('../lib/options') +const MNEMONIC_PATH = process.env.MNEMONIC_PATH const defaultPrefixPath = "m/44'/60'/1'/0'" const paymentPrefixPath = "m/44'/60'/0'/0'" const address = process.argv[2] -if (!options || !options.mnemonicPath) { +if (!MNEMONIC_PATH) { console.error(`Unable to fetch mnemonic from your account!`) process.exit(1) } @@ -47,7 +49,7 @@ function searchForHdIndex (address) { } function fetchMnemonic () { - return fs.readFile(options.mnemonicPath, 'utf8') + return fs.readFile(MNEMONIC_PATH, 'utf8') .then(mnemonic => computeSeed(mnemonic)) } diff --git a/bin/lamassu-migrate b/bin/lamassu-migrate index 3138efc8..e09c1fdf 100755 --- a/bin/lamassu-migrate +++ b/bin/lamassu-migrate @@ -1,10 +1,16 @@ #!/usr/bin/env node const FileStore = require('migrate/lib/file-store') +const _ = require('lodash/fp') +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + const db = require('../lib/db') const migrate = require('../lib/migrate') -const options = require('../lib/options') const { asyncLocalStorage, defaultStore } = require('../lib/async-storage') + +const MIGRATE_STATE_PATH = process.env.MIGRATE_STATE_PATH + const createMigration = `CREATE TABLE IF NOT EXISTS migrations ( id serial PRIMARY KEY, data json NOT NULL @@ -14,7 +20,7 @@ const select = 'select * from migrations limit 1' const getMigrateFile = () => { return new Promise((resolve, reject) => { - new FileStore(options.migrateStatePath).load((err, store) => { + new FileStore(MIGRATE_STATE_PATH).load((err, store) => { if (err) return reject(err) return resolve(store) }) @@ -26,7 +32,7 @@ asyncLocalStorage.run(store, () => { db.none(createMigration) .then(() => Promise.all([db.oneOrNone(select), getMigrateFile()])) .then(([qResult, migrateFile]) => { - process.env.SKIP_SERVER_LOGS = !(qResult && qResult.data.migrations.find(({ title }) => title === '1572524820075-server-support-logs.js')) + process.env.SKIP_SERVER_LOGS = !(qResult && _.find(({ title }) => title === '1572524820075-server-support-logs.js', qResult.data.migrations ?? [])) if (!qResult && migrateFile) { return db.none('insert into migrations (id, data) values (1, $1)', [migrateFile]) } diff --git a/bin/lamassu-mnemonic b/bin/lamassu-mnemonic index 500d07bb..fa841b08 100755 --- a/bin/lamassu-mnemonic +++ b/bin/lamassu-mnemonic @@ -1,8 +1,10 @@ #!/usr/bin/env node const fs = require('fs') +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) -const options = require('../lib/options') +const MNEMONIC_PATH = process.env.MNEMONIC_PATH -const mnemonic = fs.readFileSync(options.mnemonicPath, 'utf8').trim() +const mnemonic = fs.readFileSync(MNEMONIC_PATH, 'utf8').trim() console.log(mnemonic) diff --git a/bin/lamassu-ofac-update-sources b/bin/lamassu-ofac-update-sources index 83f6c011..0a7f9528 100755 --- a/bin/lamassu-ofac-update-sources +++ b/bin/lamassu-ofac-update-sources @@ -2,22 +2,12 @@ 'use strict' -const fs = require('fs') +const setEnvVariable = require('../tools/set-env-var') -const ofacSources = [ - { - name: 'sdn_advanced', - url: 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml' - }, - { - name: 'cons_advanced', - url: 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml' - } -] +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) -const options = require('../lib/options-loader')() - -if (!options.opts.ofacSources) { - options.opts.ofacSources = ofacSources - fs.writeFileSync(options.path, JSON.stringify(options.opts, null, ' '), 'utf8') +if (!process.env.OFAC_SOURCES_NAMES && !process.env.OFAC_SOURCES_URLS) { + setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced') + setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml') } diff --git a/bin/lamassu-operator b/bin/lamassu-operator index 104a1c5e..8249ee64 100644 --- a/bin/lamassu-operator +++ b/bin/lamassu-operator @@ -3,10 +3,14 @@ const fs = require('fs') const hkdf = require('futoin-hkdf') -const options = require('../lib/options') +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + const mnemonicHelpers = require('../lib/mnemonic-helpers') -const mnemonic = fs.readFileSync(options.mnemonicPath, 'utf8').trim() +const MNEMONIC_PATH = process.env.MNEMONIC_PATH + +const mnemonic = fs.readFileSync(MNEMONIC_PATH, 'utf8').trim() const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) console.log(hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')) diff --git a/bin/lamassu-register b/bin/lamassu-register index 7d5284bc..66af20ef 100755 --- a/bin/lamassu-register +++ b/bin/lamassu-register @@ -1,16 +1,17 @@ #!/usr/bin/env node +const path = require('path') +require('dotenv').config({ path: 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 options = require('../lib/options') const name = process.argv[2] const role = process.argv[3] -const domain = options.hostname +const domain = process.env.HOSTNAME if (!domain) { - console.error('No hostname configured in lamassu.json') + console.error('No hostname configured in the environment') process.exit(1) } diff --git a/bin/lamassu-update-to-mnemonic b/bin/lamassu-update-to-mnemonic index d4f30ed1..ed14222e 100755 --- a/bin/lamassu-update-to-mnemonic +++ b/bin/lamassu-update-to-mnemonic @@ -6,25 +6,26 @@ const fs = require('fs') const path = require('path') const os = require('os') const mnemonicHelpers = require('../lib/mnemonic-helpers') -const options = require('../lib/options-loader')() +const setEnvVariable = require('../tools/set-env-var') -if (!options.opts.mnemonicPath && options.opts.seedPath) { - const seed = fs.readFileSync(options.opts.seedPath, 'utf8').trim() +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + +if (!process.env.MNEMONIC_PATH && process.env.SEED_PATH) { + const seed = fs.readFileSync(process.env.SEED_PATH, 'utf8').trim() const mnemonic = mnemonicHelpers.fromSeed(seed) if (process.argv[2] === '--prod') { - options.opts.mnemonicPath = path.resolve('/etc', 'lamassu', 'mnemonics', 'mnemonic.txt') + setEnvVariable('MNEMONIC_PATH', path.resolve('/etc', 'lamassu', 'mnemonics', 'mnemonic.txt')) } else { - options.opts.mnemonicPath = path.resolve(os.homedir(), '.lamassu', 'mnemonics', 'mnemonic.txt') + setEnvVariable('MNEMONIC_PATH', path.resolve(os.homedir(), '.lamassu', 'mnemonics', 'mnemonic.txt')) } - if (!fs.existsSync(path.dirname(options.opts.mnemonicPath))) { - fs.mkdirSync(path.dirname(options.opts.mnemonicPath)) + if (!fs.existsSync(path.dirname(process.env.MNEMONIC_PATH))) { + fs.mkdirSync(path.dirname(process.env.MNEMONIC_PATH)) } - if (!fs.existsSync(options.opts.mnemonicPath)) { - fs.writeFileSync(options.opts.mnemonicPath, mnemonic, 'utf8') + if (!fs.existsSync(process.env.MNEMONIC_PATH)) { + fs.writeFileSync(process.env.MNEMONIC_PATH, mnemonic, 'utf8') } - - fs.writeFileSync(options.path, JSON.stringify(options.opts, null, '\t'), 'utf8') } diff --git a/bin/migrate-config.js b/bin/migrate-config.js index 32e89ae2..759e5e31 100644 --- a/bin/migrate-config.js +++ b/bin/migrate-config.js @@ -3,9 +3,13 @@ 'use strict' const pgp = require('pg-promise')() -const psqlUrl = require('../lib/options').postgresql -const db = pgp(psqlUrl) +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + +const { PSQL_URL } = require('../lib/constants') + +const db = pgp(PSQL_URL) db.many('select data from user_config', 'exchanges') .then(rows => { diff --git a/bin/old-lamassu-register b/bin/old-lamassu-register index 119632f3..e6e2313a 100755 --- a/bin/old-lamassu-register +++ b/bin/old-lamassu-register @@ -1,13 +1,15 @@ #!/usr/bin/env node +const path = require('path') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + const login = require('../lib/admin/login') -const options = require('../lib/options') const name = process.argv[2] -const domain = options.hostname +const domain = process.env.HOSTNAME if (!domain) { - console.error('No hostname configured in lamassu.json') + console.error('No hostname configured in the environment') process.exit(1) } diff --git a/dev/notify.js b/dev/notify.js index d99f77b7..5bb41bcf 100644 --- a/dev/notify.js +++ b/dev/notify.js @@ -2,7 +2,7 @@ require('es6-promise').polyfill() var notifier = require('../lib/notifier') var db = require('../lib/postgresql_interface') -var psqlUrl = require('../lib/options').postgres +const { PSQL_URL } = require('../lib/constants') function getBalances () { return [ @@ -11,7 +11,7 @@ function getBalances () { ] } -db.init(psqlUrl) +db.init(PSQL_URL) notifier.init(db, getBalances, {lowBalanceThreshold: 10}) console.log('DEBUG0') notifier.checkStatus() diff --git a/dev/recreate-seeds.js b/dev/recreate-seeds.js index 7ed801c6..b4027093 100644 --- a/dev/recreate-seeds.js +++ b/dev/recreate-seeds.js @@ -6,21 +6,21 @@ const fs = require('fs') const path = require('path') const os = require('os') const bip39 = require('bip39') -const options = require('../lib/options-loader')() +const setEnvVariable = require('../tools/set-env-var') -if (options.opts.mnemonicPath && !options.opts.seedPath) { - const mnemonic = fs.readFileSync(options.opts.mnemonicPath, 'utf8') +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + +if (process.env.MNEMONIC_PATH && !process.env.SEED_PATH) { + const mnemonic = fs.readFileSync(process.env.MNEMONIC_PATH, 'utf8') const seed = bip39.mnemonicToEntropy(mnemonic.split('\n').join(' ').trim()).toString('hex') - options.opts.seedPath = path.resolve(os.homedir(), '.lamassu', 'seeds', 'seed.txt') + setEnvVariable('SEED_PATH', path.resolve(os.homedir(), '.lamassu', 'seeds', 'seed.txt')) - if (!fs.existsSync(path.dirname(options.opts.seedPath))) { - fs.mkdirSync(path.dirname(options.opts.seedPath)) + if (!fs.existsSync(path.dirname(process.env.SEED_PATH))) { + fs.mkdirSync(path.dirname(process.env.SEED_PATH)) } - if (!fs.existsSync(options.opts.seedPath)) { - fs.writeFileSync(options.opts.seedPath, seed, 'utf8') + if (!fs.existsSync(process.env.SEED_PATH)) { + fs.writeFileSync(process.env.SEED_PATH, seed, 'utf8') } - - fs.writeFileSync(options.path, JSON.stringify(options.opts, null, '\t'), 'utf8') } diff --git a/lib/admin/admin-server.js b/lib/admin/admin-server.js index f62ba24e..a510ed00 100644 --- a/lib/admin/admin-server.js +++ b/lib/admin/admin-server.js @@ -20,7 +20,6 @@ const _ = require('lodash/fp') const machineLoader = require('../machine-loader') const T = require('../time') const logger = require('../logger') -const options = require('../options') const accounts = require('./accounts') const config = require('./config') @@ -35,18 +34,20 @@ const supportServer = require('./admin-support') const NEVER = new Date(Date.now() + 100 * T.years) const REAUTHENTICATE_INTERVAL = T.minute -const idPhotoCardBasedir = _.get('idPhotoCardDir', options) -const frontCameraBasedir = _.get('frontCameraDir', options) -const operatorDataBasedir = _.get('operatorDataDir', options) + +const HOSTNAME = process.env.HOSTNAME +const KEY_PATH = process.env.KEY_PATH +const CERT_PATH = process.env.CERT_PATH +const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR +const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR +const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR const devMode = argv.dev const version = require('../../package.json').version logger.info('Version: %s', version) -const hostname = options.hostname - -if (!hostname) { +if (!HOSTNAME) { logger.error('no hostname specified.') process.exit(1) } @@ -82,7 +83,7 @@ app.get('/api/totem', (req, res) => { if (!name) return res.status(400).send('Name is required') - return pairing.totem(hostname, name) + return pairing.totem(HOSTNAME, name) .then(totem => res.send(totem)) }) @@ -222,27 +223,27 @@ app.use((err, req, res, next) => { }) const certOptions = { - key: fs.readFileSync(options.keyPath), - cert: fs.readFileSync(options.certPath) + key: fs.readFileSync(KEY_PATH), + cert: fs.readFileSync(CERT_PATH) } app.use(serveStatic(path.resolve(__dirname, 'public'))) -if (!fs.existsSync(idPhotoCardBasedir)) { - makeDir.sync(idPhotoCardBasedir) +if (!fs.existsSync(ID_PHOTO_CARD_DIR)) { + makeDir.sync(ID_PHOTO_CARD_DIR) } -if (!fs.existsSync(frontCameraBasedir)) { - makeDir.sync(frontCameraBasedir) +if (!fs.existsSync(FRONT_CAMERA_DIR)) { + makeDir.sync(FRONT_CAMERA_DIR) } -if (!fs.existsSync(operatorDataBasedir)) { - makeDir.sync(operatorDataBasedir) +if (!fs.existsSync(OPERATOR_DATA_DIR)) { + makeDir.sync(OPERATOR_DATA_DIR) } -app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, {index: false})) -app.use('/front-camera-photo', serveStatic(frontCameraBasedir, {index: false})) -app.use('/operator-data', serveStatic(operatorDataBasedir, {index: false})) +app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, {index: false})) +app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, {index: false})) +app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, {index: false})) function register (req, res, next) { const otp = req.query.otp @@ -259,7 +260,7 @@ function register (req, res, next) { const cookieOpts = { httpOnly: true, secure: true, - domain: hostname, + domain: HOSTNAME, sameSite: true, expires: NEVER } diff --git a/lib/admin/admin-support.js b/lib/admin/admin-support.js index cd738610..13fd5f9d 100644 --- a/lib/admin/admin-support.js +++ b/lib/admin/admin-support.js @@ -9,7 +9,9 @@ const _ = require('lodash/fp') const serveStatic = require('serve-static') const path = require('path') -const options = require('../options') +const KEY_PATH = process.env.KEY_PATH +const CERT_PATH = process.env.CERT_PATH +const LAMASSU_CA_PATH = process.env.LAMASSU_CA_PATH app.use(morgan('dev')) app.use(helmet({noCache: true})) @@ -20,9 +22,9 @@ app.use(serveStatic(path.resolve(__dirname, '..', '..', 'public'), { })) const certOptions = { - key: fs.readFileSync(options.keyPath), - cert: fs.readFileSync(options.certPath), - ca: [fs.readFileSync(options.lamassuCaPath)], + key: fs.readFileSync(KEY_PATH), + cert: fs.readFileSync(CERT_PATH), + ca: [fs.readFileSync(LAMASSU_CA_PATH)], requestCert: true, rejectUnauthorized: true } diff --git a/lib/admin/pairing.js b/lib/admin/pairing.js index 0bdade02..7d520bbf 100644 --- a/lib/admin/pairing.js +++ b/lib/admin/pairing.js @@ -4,19 +4,18 @@ const readFile = pify(fs.readFile) const crypto = require('crypto') const baseX = require('base-x') -const options = require('../options') const db = require('../db') const pairing = require('../pairing') const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:' const bsAlpha = baseX(ALPHA_BASE) +const CA_PATH = process.env.CA_PATH + const unpair = pairing.unpair function totem (hostname, name) { - const caPath = options.caPath - - return readFile(caPath) + return readFile(CA_PATH) .then(data => { const caHash = crypto.createHash('sha256').update(data).digest() const token = crypto.randomBytes(32) diff --git a/lib/app.js b/lib/app.js index 72394605..449da86a 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,8 +1,11 @@ const fs = require('fs') +const path = require('path') const http = require('http') const https = require('https') const argv = require('minimist')(process.argv.slice(2)) +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) + const { asyncLocalStorage, defaultStore } = require('./async-storage') const routes = require('./routes') const logger = require('./logger') @@ -10,11 +13,13 @@ const poller = require('./poller') const settingsLoader = require('./new-settings-loader') const configManager = require('./new-config-manager') const complianceTriggers = require('./compliance-triggers') -const options = require('./options') const ofac = require('./ofac/index') const ofacUpdate = require('./ofac/update') -const devMode = argv.dev || options.http +const KEY_PATH = process.env.KEY_PATH +const CERT_PATH = process.env.CERT_PATH + +const devMode = argv.dev || process.env.HTTP const version = require('../package.json').version logger.info('Version: %s', version) @@ -66,8 +71,8 @@ function startServer (settings) { .then(() => { poller.setup(['public']) const httpsServerOptions = { - key: fs.readFileSync(options.keyPath), - cert: fs.readFileSync(options.certPath), + key: fs.readFileSync(KEY_PATH), + cert: fs.readFileSync(CERT_PATH), requestCert: true, rejectUnauthorized: false } @@ -80,7 +85,7 @@ function startServer (settings) { const localPort = 3030 const localServer = http.createServer(routes.localApp) - if (options.devMode) logger.info('In dev mode') + if (devMode) logger.info('In dev mode') server.listen(port, () => { logger.info('lamassu-server listening on port ' + diff --git a/lib/blockchain/do-volume.js b/lib/blockchain/do-volume.js index 061818c0..e3f5b825 100644 --- a/lib/blockchain/do-volume.js +++ b/lib/blockchain/do-volume.js @@ -1,10 +1,10 @@ const fs = require('fs') -const options = require('../options') - const common = require('./common') -const MOUNT_POINT = options.blockchainDir +const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR + +const MOUNT_POINT = BLOCKCHAIN_DIR module.exports = {prepareVolume} diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index 1191f20e..90cd1945 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -8,7 +8,6 @@ const inquirer = require('inquirer') const _ = require('lodash/fp') const { utils: coinUtils } = require('@lamassu/coins') -const options = require('../options') const settingsLoader = require('../new-settings-loader') const wallet = require('../wallet') @@ -29,10 +28,12 @@ const PLUGINS = { ZEC: require('./zcash.js') } +const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR + module.exports = {run} function installedVolumeFilePath (crypto) { - return path.resolve(coinUtils.cryptoDir(crypto, options.blockchainDir), '.installed') + return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed') } function isInstalledVolume (crypto) { @@ -63,7 +64,7 @@ function processCryptos (codes) { common.es('sudo supervisorctl reread') common.es('sudo supervisorctl update') - const blockchainDir = options.blockchainDir + const blockchainDir = BLOCKCHAIN_DIR const backupDir = path.resolve(os.homedir(), 'backups') const rsyncCmd = `( \ (crontab -l 2>/dev/null || echo -n "") | grep -v "@daily rsync ".*"wallet.dat"; \ @@ -83,7 +84,7 @@ function processCryptos (codes) { function setupCrypto (crypto) { logger.info(`Installing ${crypto.display}...`) - const cryptoDir = coinUtils.cryptoDir(crypto, options.blockchainDir) + const cryptoDir = coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR) makeDir.sync(cryptoDir) const cryptoPlugin = plugin(crypto) const oldDir = process.cwd() diff --git a/lib/coinatmradar/coinatmradar.js b/lib/coinatmradar/coinatmradar.js index 64660f4e..476cfd55 100644 --- a/lib/coinatmradar/coinatmradar.js +++ b/lib/coinatmradar/coinatmradar.js @@ -1,13 +1,9 @@ const axios = require('axios') const _ = require('lodash/fp') -const pify = require('pify') -const fs = pify(require('fs')) - const db = require('../db') const configManager = require('../new-config-manager') const complianceTriggers = require('../compliance-triggers') -const options = require('../options') const logger = require('../logger') const plugins = require('../plugins') const { getOperatorId } = require('../operator') @@ -15,6 +11,8 @@ const { getOperatorId } = require('../operator') const TIMEOUT = 10000 const MAX_CONTENT_LENGTH = 2000 +const COIN_ATM_RADAR_URL = process.env.COIN_ATM_RADAR_URL + // How long a machine can be down before it's considered offline const STALE_INTERVAL = '2 minutes' @@ -112,7 +110,7 @@ function getMachines (rates, settings) { } function sendRadar (data) { - const url = _.get(['coinAtmRadar', 'url'], options) + const url = COIN_ATM_RADAR_URL if (_.isEmpty(url)) { return Promise.reject(new Error('Missing coinAtmRadar url!')) diff --git a/lib/constants.js b/lib/constants.js index c4bc2fbc..ff1bc8f2 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,5 +1,13 @@ const T = require('./time') +const POSTGRES_USER = process.env.POSTGRES_USER +const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD +const POSTGRES_HOST = process.env.POSTGRES_HOST +const POSTGRES_PORT = process.env.POSTGRES_PORT +const POSTGRES_DB = process.env.POSTGRES_DB + +const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}` + const anonymousCustomer = { uuid: '47ac1184-8102-11e7-9079-8f13a7117867', name: 'anonymous' @@ -39,5 +47,6 @@ module.exports = { CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, WALLET_SCORE_THRESHOLD, - RECEIPT + RECEIPT, + PSQL_URL } diff --git a/lib/customers.js b/lib/customers.js index 0f68f1ed..7246e021 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -13,20 +13,20 @@ const BN = require('./bn') const anonymous = require('../lib/constants').anonymousCustomer const complianceOverrides = require('./compliance_overrides') const users = require('./users') -const options = require('./options') const writeFile = util.promisify(fs.writeFile) const notifierQueries = require('./notifier/queries') const notifierUtils = require('./notifier/utils') const NUM_RESULTS = 1000 -const idPhotoCardBasedir = _.get('idPhotoCardDir', options) -const frontCameraBaseDir = _.get('frontCameraDir', options) -const operatorDataDir = _.get('operatorDataDir', options) const sms = require('./sms') const settingsLoader = require('./new-settings-loader') const logger = require('./logger') const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel'] +const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR +const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR +const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR + /** * Add new customer * @@ -673,7 +673,7 @@ function updatePhotoCard (id, patch) { const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) // i.e. ..//idphotocard/24/0e/85 - const dirname = path.join(idPhotoCardBasedir, rpath) + const dirname = path.join(ID_PHOTO_CARD_DIR, rpath) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) @@ -736,7 +736,7 @@ function updatePhotos (imagesData, id, dir) { function updateIdCardData (patch, id) { /* TODO: fetch operator id */ const operatorId = 'id-operator' - const directory = `${operatorDataDir}/${operatorId}/${id}/` + const directory = `${OPERATOR_DATA_DIR}/${operatorId}/${id}/` return Promise.resolve(patch) .then(patch => { @@ -755,7 +755,7 @@ function updateTxCustomerPhoto (imageData) { return Promise.resolve(imageData) .then(imageData => { const newPatch = {} - const directory = `${operatorDataDir}/customersphotos` + const directory = `${OPERATOR_DATA_DIR}/customersphotos` if (_.isEmpty(imageData)) { return @@ -826,7 +826,7 @@ function updateFrontCamera (id, patch) { const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) // i.e. ..//idphotocard/24/0e/85 - const dirname = path.join(frontCameraBaseDir, rpath) + const dirname = path.join(FRONT_CAMERA_DIR, rpath) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) diff --git a/lib/db.js b/lib/db.js index 43c25537..4a66e566 100644 --- a/lib/db.js +++ b/lib/db.js @@ -2,7 +2,7 @@ const Pgp = require('pg-promise') const uuid = require('uuid') const _ = require('lodash/fp') -const psqlUrl = require('../lib/options').postgresql +const { PSQL_URL } = require('./constants') const logger = require('./logger') const eventBus = require('./event-bus') const { asyncLocalStorage, defaultStore } = require('./async-storage') @@ -87,7 +87,7 @@ const pgp = Pgp({ } }) -const db = stripDefaultDbFuncs(pgp(psqlUrl)) +const db = stripDefaultDbFuncs(pgp(PSQL_URL)) eventBus.subscribe('log', args => { if (process.env.SKIP_SERVER_LOGS) return diff --git a/lib/logger.js b/lib/logger.js index 5b21e3c1..65298e64 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,9 +1,11 @@ const winston = require('winston') const Postgres = require('./pg-transport') -const options = require('./options') +const { PSQL_URL } = require('./constants') + +const LOG_LEVEL = process.env.LOG_LEVEL const logger = new winston.Logger({ - level: options.logLevel, + level: LOG_LEVEL, transports: [ new (winston.transports.Console)({ timestamp: true, @@ -12,7 +14,7 @@ const logger = new winston.Logger({ humanReadableUnhandledException: true }), new Postgres({ - connectionString: options.postgresql, + connectionString: PSQL_URL, tableName: 'server_logs', handleExceptions: true, humanReadableUnhandledException: true diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 53f86ccc..b6ec0221 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -8,13 +8,13 @@ const cors = require('cors') const helmet = require('helmet') const nocache = require('nocache') const cookieParser = require('cookie-parser') -const { ApolloServer, AuthenticationError } = require('apollo-server-express') const { graphqlUploadExpress } = require('graphql-upload') +const { ApolloServer } = require('apollo-server-express') const _ = require('lodash/fp') +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }) + const { asyncLocalStorage, defaultStore } = require('../async-storage') -const options = require('../options') -const users = require('../users') const logger = require('../logger') const { AuthDirective } = require('./graphql/directives') @@ -25,13 +25,16 @@ const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants') const { session, cleanUserSessions, buildApolloContext } = require('./middlewares') const devMode = require('minimist')(process.argv.slice(2)).dev -const idPhotoCardBasedir = _.get('idPhotoCardDir', options) -const frontCameraBasedir = _.get('frontCameraDir', options) -const operatorDataBasedir = _.get('operatorDataDir', options) -const hostname = options.hostname -if (!hostname) { - logger.error('no hostname specified.') +const HOSTNAME = process.env.HOSTNAME +const KEY_PATH = process.env.KEY_PATH +const CERT_PATH = process.env.CERT_PATH +const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR +const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR +const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR + +if (!HOSTNAME) { + logger.error('No hostname specified.') process.exit(1) } @@ -77,16 +80,16 @@ apolloServer.applyMiddleware({ // cors on app for /api/register endpoint. app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' })) -app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false })) -app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })) -app.use('/operator-data', serveStatic(operatorDataBasedir, { index: false })) +app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false })) +app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false })) +app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false })) // Everything not on graphql or api/register is redirected to the front-end app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html'))) const certOptions = { - key: fs.readFileSync(options.keyPath), - cert: fs.readFileSync(options.certPath) + key: fs.readFileSync(KEY_PATH), + cert: fs.readFileSync(CERT_PATH) } function run () { diff --git a/lib/new-admin/graphql-dev-insecure.js b/lib/new-admin/graphql-dev-insecure.js index 8ec2b586..8d673d54 100644 --- a/lib/new-admin/graphql-dev-insecure.js +++ b/lib/new-admin/graphql-dev-insecure.js @@ -1,6 +1,9 @@ const express = require('express') +const path = require('path') const { ApolloServer } = require('apollo-server-express') +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }) + const { typeDefs, resolvers } = require('./graphql/schema') const logger = require('../logger') diff --git a/lib/new-admin/middlewares/session.js b/lib/new-admin/middlewares/session.js index 338e4b11..f85b6205 100644 --- a/lib/new-admin/middlewares/session.js +++ b/lib/new-admin/middlewares/session.js @@ -3,12 +3,9 @@ const router = express.Router() const session = require('express-session') const PgSession = require('connect-pg-simple')(session) const db = require('../../db') -const options = require('../../options') const { USER_SESSIONS_TABLE_NAME } = require('../../constants') const { getOperatorId } = require('../../operator') -const hostname = options.hostname - router.use('*', async (req, res, next) => getOperatorId('authentication').then(({ operatorId }) => session({ store: new PgSession({ pgPromise: db, diff --git a/lib/new-admin/services/pairing.js b/lib/new-admin/services/pairing.js index 0ae14dc0..d267c1a9 100644 --- a/lib/new-admin/services/pairing.js +++ b/lib/new-admin/services/pairing.js @@ -5,24 +5,25 @@ const crypto = require('crypto') const baseX = require('base-x') const { parse, NIL } = require('uuid') -const options = require('../../options') const db = require('../../db') const pairing = require('../../pairing') const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:' const bsAlpha = baseX(ALPHA_BASE) +const CA_PATH = process.env.CA_PATH +const HOSTNAME = process.env.HOSTNAME + const unpair = pairing.unpair function totem (name) { - const caPath = options.caPath - return readFile(caPath) + return readFile(CA_PATH) .then(data => { const caHash = crypto.createHash('sha256').update(data).digest() const token = crypto.randomBytes(32) const hexToken = token.toString('hex') const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex') - const buf = Buffer.concat([caHash, token, Buffer.from(options.hostname)]) + const buf = Buffer.concat([caHash, token, Buffer.from(HOSTNAME)]) const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)' return db.none(sql, [hexToken, caHexToken, name]) diff --git a/lib/ofac/index.js b/lib/ofac/index.js index 4d352263..fff2f70e 100644 --- a/lib/ofac/index.js +++ b/lib/ofac/index.js @@ -4,23 +4,24 @@ const util = require('util') const loader = require('./loading') const matcher = require('./matching') const nameUtils = require('./name-utils') -const options = require('../options') const _ = require('lodash/fp') const logger = require('../logger') const debugLog = require('../pp')(__filename) // KOSTIS TODO: remove +const OFAC_DATA_DIR = process.env.OFAC_DATA_DIR + let structs = null const readdir = util.promisify(fs.readdir) function load () { - if (!options.ofacDataDir) { - const message = 'The ofacDataDir option has not been set in lamassu.json' + if (!OFAC_DATA_DIR) { + const message = 'The ofacDataDir option has not been set in the environment' return Promise.reject(new Error(message)) } - const ofacSourcesDir = path.join(options.ofacDataDir, 'sources') + const ofacSourcesDir = path.join(OFAC_DATA_DIR, 'sources') return readdir(ofacSourcesDir) .then(_.flow( diff --git a/lib/ofac/update.js b/lib/ofac/update.js index 5cf7a351..11fe480d 100644 --- a/lib/ofac/update.js +++ b/lib/ofac/update.js @@ -4,12 +4,23 @@ const url = require('url') const fs = require('fs') const path = require('path') const util = require('util') -const options = require('../options') const _ = require('lodash/fp') const logger = require('../logger') const DOWNLOAD_DIR = path.resolve('/tmp') +const OFAC_DATA_DIR = process.env.OFAC_DATA_DIR +const OFAC_SOURCES_NAMES = process.env.OFAC_SOURCES_NAMES.split(',') +const OFAC_SOURCES_URLS = process.env.OFAC_SOURCES_URLS.split(',') + +const ofacSources = _.map( + it => ({ + name: it[0], + url: it[1] + }), + _.zip(OFAC_SOURCES_NAMES, OFAC_SOURCES_URLS) +) + function mkdir (path) { return new Promise((resolve, reject) => { fs.mkdir(path, err => { @@ -97,14 +108,12 @@ const moveToSourcesDir = (srcFile, ofacSourcesDir) => { } function update () { - const OFAC_DATA_DIR = options.ofacDataDir - if (!OFAC_DATA_DIR) { - throw new Error('ofacDataDir must be defined in lamassu.json') + throw new Error('ofacDataDir must be defined in the environment') } - if (!options.ofacSources) { - logger.error('ofacSources must be defined in lamassu.json') + if (!ofacSources) { + logger.error('ofacSources must be defined in the environment') } const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources') @@ -125,7 +134,7 @@ function update () { return {} }) - const promiseNewEtags = Promise.resolve(options.ofacSources || []) + const promiseNewEtags = Promise.resolve(ofacSources || []) .then(sources => Promise.all(_.map(promiseGetEtag, sources)) .then(etags => _.map( ([source, etag]) => ({...source, etag}), diff --git a/lib/options-loader.js b/lib/options-loader.js index 9b9af1be..4f05ee5a 100644 --- a/lib/options-loader.js +++ b/lib/options-loader.js @@ -4,11 +4,12 @@ const os = require('os') const argv = require('minimist')(process.argv.slice(2)) const _ = require('lodash/fp') -require('dotenv').config() +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) const DATABASE = process.env.LAMASSU_DB ?? 'PROD' const dbMapping = psqlConf => ({ STRESS_TEST: _.replace('lamassu', 'lamassu_stress', psqlConf), + PAZUZ: _.replace('lamassu', 'lamassu_pazuz', psqlConf), RELEASE: _.replace('lamassu', 'lamassu_release', psqlConf), DEV: _.replace('lamassu', 'lamassu', psqlConf), PROD: _.replace('lamassu', 'lamassu', psqlConf) diff --git a/lib/pairing.js b/lib/pairing.js index 55cddc98..f6942591 100644 --- a/lib/pairing.js +++ b/lib/pairing.js @@ -2,13 +2,14 @@ const fs = require('fs') const pify = require('pify') const readFile = pify(fs.readFile) const db = require('./db') -const options = require('./options') const logger = require('./logger') const uuid = require('uuid') +const CA_PATH = process.env.CA_PATH + // A machine on an older version (no multicassette code) could be paired with a server with multicassette code. // This makes sure that the server stores a default value -const DEFAULT_NUMBER_OF_CASSETTES = 2 +const DEFAULT_NUMBER_OF_CASSETTES = 2 function pullToken (token) { const sql = `delete from pairing_tokens @@ -58,8 +59,7 @@ function authorizeCaDownload (caToken) { .then(r => { if (r.expired) throw new Error('Expired') - const caPath = options.caPath - return readFile(caPath, {encoding: 'utf8'}) + return readFile(CA_PATH, {encoding: 'utf8'}) }) } diff --git a/lib/plugins/common/json-rpc.js b/lib/plugins/common/json-rpc.js index 50372116..be82ac0b 100644 --- a/lib/plugins/common/json-rpc.js +++ b/lib/plugins/common/json-rpc.js @@ -6,9 +6,7 @@ const _ = require('lodash/fp') const request = require('request-promise') const { utils: coinUtils } = require('@lamassu/coins') -const options = require('../../options') - -const blockchainDir = options.blockchainDir +const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR module.exports = { fetch, fetchDigest, parseConf, rpcConfig @@ -108,7 +106,7 @@ function parseConf (confPath) { function rpcConfig (cryptoRec) { try { - const configPath = coinUtils.configPath(cryptoRec, blockchainDir) + const configPath = coinUtils.configPath(cryptoRec, BLOCKCHAIN_DIR) const config = parseConf(configPath) return { username: config.rpcuser, diff --git a/lib/plugins/layer2/strike/strike.js b/lib/plugins/layer2/strike/strike.js index 39f3cd01..d3469331 100644 --- a/lib/plugins/layer2/strike/strike.js +++ b/lib/plugins/layer2/strike/strike.js @@ -1,7 +1,7 @@ const axios = require('axios') const _ = require('lodash/fp') -const options = require('../../../options') +const STRIKE_BASE_URL = process.env.STRIKE_BASE_URL module.exports = { newAddress, @@ -9,7 +9,7 @@ module.exports = { cryptoNetwork } -axios.defaults.baseURL = _.get('strike.baseUrl', options) +axios.defaults.baseURL = STRIKE_BASE_URL if (_.isEmpty(axios.defaults.baseURL)) { throw new Error('Missing Strike baseUrl!') } diff --git a/lib/plugins/wallet/lnd/lnd.js b/lib/plugins/wallet/lnd/lnd.js index 8f7d3e72..f32cee37 100644 --- a/lib/plugins/wallet/lnd/lnd.js +++ b/lib/plugins/wallet/lnd/lnd.js @@ -3,13 +3,14 @@ const lnd = require('lnd-async') const BN = require('../../../bn') const E = require('../../../error') const { utils: coinUtils } = require('@lamassu/coins') -const options = require('../../../options') const _ = require('lodash/fp') const cryptoRec = coinUtils.getCryptoCurrency('BTC') const unitScale = cryptoRec.unitScale +const LND = process.env.LIGHTNING_NETWORK_DAEMON + module.exports = { balance, sendCoins, @@ -20,7 +21,7 @@ module.exports = { } function connect () { - return lnd.connect(options.lnd || {}) + return lnd.connect(LND || {}) } function cryptoNetwork (account, cryptoCode, settings, operatorId) { diff --git a/lib/plugins/wallet/monerod/monerod.js b/lib/plugins/wallet/monerod/monerod.js index f6f17fcd..0a5662a3 100644 --- a/lib/plugins/wallet/monerod/monerod.js +++ b/lib/plugins/wallet/monerod/monerod.js @@ -7,14 +7,13 @@ const { default: PQueue } = require('p-queue') const BN = require('../../../bn') const E = require('../../../error') const { logger } = require('../../../blockchain/common') -const options = require('../../../options') const jsonRpc = require('../../common/json-rpc') -const blockchainDir = options.blockchainDir +const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR const cryptoRec = utils.getCryptoCurrency(COINS.XMR) -const configPath = utils.configPath(cryptoRec, blockchainDir) -const walletDir = path.resolve(utils.cryptoDir(cryptoRec, blockchainDir), 'wallets') +const configPath = utils.configPath(cryptoRec, BLOCKCHAIN_DIR) +const walletDir = path.resolve(utils.cryptoDir(cryptoRec, BLOCKCHAIN_DIR), 'wallets') const DIGEST_QUEUE = new PQueue({ concurrency: 1, diff --git a/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js b/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js index 44a29f47..9ed8c3b7 100644 --- a/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js +++ b/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js @@ -4,11 +4,11 @@ const E = require('../../../error') const _ = require('lodash/fp') const ENV = process.env.NODE_ENV === undefined || process.env.NODE_ENV === 'development' ? 'development' : 'production' -const SUPPORTED_COINS = ['BTC', 'ZEC', 'LTC', 'BCH', 'DASH', 'ETH'] +const SUPPORTED_COINS = ['BTC'] const axios = require('axios').create({ // TODO: get rejectUnauthorized true to work - baseURL: ENV === 'development' ? 'https://localhost:5555/api/' : process.env.PAZUZ_API_WALLET_URL, + baseURL: `${process.env.WALLET_URL}/api`, httpsAgent: new https.Agent({ rejectUnauthorized: false }) @@ -22,9 +22,8 @@ function balance (account, cryptoCode, settings, operatorId) { return checkCryptoCode(cryptoCode) .then(() => { return axios.post('/balance', { - account, cryptoCode, - settings, + config: settings.config, operatorId }) }) @@ -39,9 +38,8 @@ function sendCoins (account, tx, settings, operatorId) { return checkCryptoCode(cryptoCode) .then(() => { return axios.post('/sendCoins', { - account, tx, - settings, + config: settings.config, operatorId }) }) @@ -57,10 +55,9 @@ function sendCoins (account, tx, settings, operatorId) { function newAddress (account, info, tx, settings, operatorId) { return checkCryptoCode(info.cryptoCode) .then(() => axios.post('/newAddress', { - account, info, tx, - settings, + config: settings.config, operatorId })) .then(({ data }) => { diff --git a/lib/routes.js b/lib/routes.js index 22b72bf2..7e5343ca 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -6,7 +6,6 @@ const morgan = require('morgan') const nocache = require('nocache') const logger = require('./logger') -const options = require('./options') const authorize = require('./middlewares/authorize') const errorHandler = require('./middlewares/errorHandler') @@ -41,7 +40,7 @@ const configRequiredRoutes = [ '/tx', '/verify_promo_code' ] -const devMode = argv.dev || options.http +const devMode = argv.dev || process.env.HTTP // middleware setup app.use(compression({ threshold: 500 })) diff --git a/lib/wallet.js b/lib/wallet.js index 1073a8a0..068ffc5b 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -7,7 +7,6 @@ const pify = require('pify') const fs = pify(require('fs')) const mnemonicHelpers = require('./mnemonic-helpers') -const options = require('./options') const ph = require('./plugin-helper') const layer2 = require('./layer2') const httpError = require('./route-helpers').httpError @@ -20,6 +19,8 @@ const INSUFFICIENT_FUNDS_CODE = 570 const INSUFFICIENT_FUNDS_NAME = 'InsufficientFunds' const ZERO_CONF_EXPIRATION = 60000 +const MNEMONIC_PATH = process.env.MNEMONIC_PATH + function computeSeed (masterSeed) { return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' }) } @@ -29,7 +30,7 @@ function computeOperatorId (masterSeed) { } function fetchWallet (settings, cryptoCode) { - return fs.readFile(options.mnemonicPath, 'utf8') + return fs.readFile(MNEMONIC_PATH, 'utf8') .then(mnemonic => { const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet diff --git a/migrations/1623413776161-create-operator-ids.js b/migrations/1623413776161-create-operator-ids.js index 7ca4a31e..c9157f0f 100644 --- a/migrations/1623413776161-create-operator-ids.js +++ b/migrations/1623413776161-create-operator-ids.js @@ -5,7 +5,6 @@ const hkdf = require('futoin-hkdf') const state = require('../lib/middlewares/state') const mnemonicHelpers = require('../lib/mnemonic-helpers') -const options = require('../lib/options') function computeOperatorId (masterSeed) { return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex') @@ -13,7 +12,7 @@ function computeOperatorId (masterSeed) { function getMnemonic () { if (state.mnemonic) return Promise.resolve(state.mnemonic) - return fs.readFile(options.mnemonicPath, 'utf8').then(mnemonic => { + return fs.readFile(process.env.MNEMONIC_PATH, 'utf8').then(mnemonic => { state.mnemonic = mnemonic return mnemonic }) 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/pages/Accounting/Accounting.js b/new-lamassu-admin/src/pages/Accounting/Accounting.js index 7175cd3d..bd141bcb 100644 --- a/new-lamassu-admin/src/pages/Accounting/Accounting.js +++ b/new-lamassu-admin/src/pages/Accounting/Accounting.js @@ -170,22 +170,26 @@ const Accounting = () => { ] return ( - <> - - -

Fiat balance history

- - + !loading && ( + <> + + +

Fiat balance history

+ + + ) ) } diff --git a/tools/build-dev-env.js b/tools/build-dev-env.js new file mode 100644 index 00000000..ddc89de2 --- /dev/null +++ b/tools/build-dev-env.js @@ -0,0 +1,32 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') + +const setEnvVariable = require('./set-env-var') + +fs.copyFileSync(path.resolve(__dirname, '../.sample.env'), path.resolve(__dirname, '../.env')) + +setEnvVariable('LAMASSU_DB', 'DEV') +setEnvVariable('POSTGRES_USER', 'postgres') +setEnvVariable('POSTGRES_PASSWORD', 'postgres123') +setEnvVariable('POSTGRES_HOST', 'localhost') +setEnvVariable('POSTGRES_PORT', '5432') +setEnvVariable('POSTGRES_DB', 'lamassu') + +setEnvVariable('CA_PATH', `${process.env.PWD}/certs/Lamassu_OP_Root_CA.pem`) +setEnvVariable('CERT_PATH', `${process.env.PWD}/certs/Lamassu_OP.pem`) +setEnvVariable('KEY_PATH', `${process.env.PWD}/certs/Lamassu_OP.key`) + +setEnvVariable('MNEMONIC_PATH', `${process.env.HOME}/.lamassu/mnemonics/mnemonic.txt`) +setEnvVariable('MIGRATE_STATE_PATH', `${process.env.HOME}/.lamassu/.migrate`) + +setEnvVariable('OFAC_DATA_DIR', `${process.env.HOME}/.lamassu/ofac`) +setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.HOME}/.lamassu/idphotocard`) +setEnvVariable('FRONT_CAMERA_DIR', `${process.env.HOME}/.lamassu/frontcamera`) +setEnvVariable('OPERATOR_DATA_DIR', `${process.env.HOME}/.lamassu/operatordata`) + +setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced') +setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml') + +setEnvVariable('HOSTNAME', 'localhost') +setEnvVariable('LOG_LEVEL', 'debug') diff --git a/tools/build-prod-env.js b/tools/build-prod-env.js new file mode 100644 index 00000000..ba277224 --- /dev/null +++ b/tools/build-prod-env.js @@ -0,0 +1,45 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') +const argv = require('minimist')(process.argv.slice(2)) +const _ = require('lodash/fp') + +const setEnvVariable = require('./set-env-var') + +const requiredParams = ['db-password', 'hostname'] + +if (!_.isEqual(_.intersection(_.keys(argv), requiredParams), requiredParams)) { + console.error('Usage: node tools/build-prod-env.js --db-password --hostname ') + process.exit(2) +} + +fs.copyFileSync(path.resolve(__dirname, '../.sample.env'), path.resolve(__dirname, '../.env')) + +setEnvVariable('POSTGRES_USER', 'lamassu_pg') +setEnvVariable('POSTGRES_PASSWORD', `${argv['db-password']}`) +setEnvVariable('POSTGRES_HOST', 'localhost') +setEnvVariable('POSTGRES_PORT', '5432') +setEnvVariable('POSTGRES_DB', 'lamassu') + +setEnvVariable('LAMASSU_CA_PATH', `/etc/ssl/certs/Lamassu_CA.pem`) +setEnvVariable('CA_PATH', `/etc/ssl/certs/Lamassu_OP_Root_CA.pem`) +setEnvVariable('CERT_PATH', `/etc/ssl/certs/Lamassu_OP.pem`) +setEnvVariable('KEY_PATH', `/etc/ssl/certs/Lamassu_OP.key`) + +setEnvVariable('MNEMONIC_PATH', `/etc/lamassu/mnemonics/mnemonic.txt`) +setEnvVariable('MIGRATE_STATE_PATH', `/etc/lamassu/.migrate`) + +setEnvVariable('BLOCKCHAIN_DIR', `/mnt/blockchains`) +setEnvVariable('OFAC_DATA_DIR', `/var/lamassu/ofac`) +setEnvVariable('ID_PHOTO_CARD_DIR', `/opt/lamassu-server/idphotocard`) +setEnvVariable('FRONT_CAMERA_DIR', `/opt/lamassu-server/frontcamera`) +setEnvVariable('OPERATOR_DATA_DIR', `/opt/lamassu-server/operatordata`) + +setEnvVariable('STRIKE_BASE_URL', `https://api.strike.acinq.co/api/`) +setEnvVariable('COIN_ATM_RADAR_URL', `https://coinatmradar.info/api/lamassu/`) + +setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced') +setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml') + +setEnvVariable('HOSTNAME', `${argv.hostname}`) +setEnvVariable('LOG_LEVEL', 'info') diff --git a/tools/cert-gen.sh b/tools/cert-gen.sh index 2a5fe062..90d5dde4 100755 --- a/tools/cert-gen.sh +++ b/tools/cert-gen.sh @@ -91,32 +91,6 @@ rm /tmp/Lamassu_OP.csr.pem mkdir -p $OFAC_DATA_DIR/sources touch $OFAC_DATA_DIR/etags.json -cat < $CONFIG_DIR/lamassu.json -{ - "postgresql": "psql://postgres:$POSTGRES_PASS@localhost/lamassu", - "mnemonicPath": "$MNEMONIC_FILE", - "caPath": "$CA_PATH", - "certPath": "$SERVER_CERT_PATH", - "keyPath": "$SERVER_KEY_PATH", - "hostname": "$DOMAIN", - "logLevel": "debug", - "lamassuCaPath": "$LAMASSU_CA_PATH", - "migrateStatePath": "$MIGRATE_STATE_PATH", - "ofacDataDir": "$OFAC_DATA_DIR", - "ofacSources": [ - { - "name": "sdn_advanced", - "url": "https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml" - }, - { - "name": "cons_advanced", - "url": "https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml" - } - ], - "idPhotoCardDir": "$IDPHOTOCARD_DIR", - "frontCameraDir": "$FRONTCAMERA_DIR", - "operatorDataDir": "$OPERATOR_DIR" -} -EOF +node tools/build-dev-env.js echo "Done." diff --git a/tools/set-env-var.js b/tools/set-env-var.js new file mode 100644 index 00000000..1b82cff0 --- /dev/null +++ b/tools/set-env-var.js @@ -0,0 +1,24 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') + +const setEnvVariable = (key, value) => { + const ENV_VARIABLES = fs.readFileSync(path.resolve(__dirname, '../.env'), 'utf-8').split(os.EOL) + const target = ENV_VARIABLES.indexOf(ENV_VARIABLES.find(line => line.match(new RegExp(`^${key}=`)))) + + if (target < 0) { + // The variable doesn't exist, add it + ENV_VARIABLES.push(`${key}=${value}`) + } else { + // .env already has that variable set, or at least has the definition of its key + // + // This is currently circumventing a possible bug on dotenv + // where the variables on this script were showing up as undefined on the first run despite the key existing, + // while on a second run they'd appear as empty string, as intended + ENV_VARIABLES.splice(target, 1, `${key}=${value}`) + } + + fs.writeFileSync(path.resolve(__dirname, '../.env'), ENV_VARIABLES.join(os.EOL)) +} + +module.exports = setEnvVariable From b2da82453a96165269dc3987392a8c80a01b00ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 22 Apr 2022 19:20:56 +0100 Subject: [PATCH 20/44] fix: production env file path --- bin/convert-txs.js | 2 +- bin/lamassu-configure-frontcamera | 2 +- bin/lamassu-eth-recovery | 2 +- bin/lamassu-migrate | 2 +- bin/lamassu-mnemonic | 2 +- bin/lamassu-ofac-update-sources | 2 +- bin/lamassu-operator | 2 +- bin/lamassu-register | 2 +- bin/lamassu-update-to-mnemonic | 2 +- bin/migrate-config.js | 2 +- bin/old-lamassu-register | 2 +- dev/recreate-seeds.js | 2 +- lib/app.js | 2 +- lib/migrate-options.js | 60 ++++----------------------- lib/new-admin/admin-server.js | 2 +- lib/new-admin/graphql-dev-insecure.js | 2 +- lib/options-loader.js | 2 +- tools/build-prod-env.js | 2 +- tools/set-env-var.js | 5 ++- 19 files changed, 27 insertions(+), 72 deletions(-) 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 e09c1fdf..286a6842 100755 --- a/bin/lamassu-migrate +++ b/bin/lamassu-migrate @@ -3,7 +3,7 @@ const FileStore = require('migrate/lib/file-store') const _ = require('lodash/fp') 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 db = require('../lib/db') const migrate = require('../lib/migrate') 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 66af20ef..8196b6dc 100755 --- a/bin/lamassu-register +++ b/bin/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 { asyncLocalStorage, defaultStore } = require('../lib/async-storage') const userManagement = require('../lib/new-admin/graphql/modules/userManagement') const authErrors = require('../lib/new-admin/graphql/errors/authentication') 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..f910164c 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') 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/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/tools/build-prod-env.js b/tools/build-prod-env.js index ba277224..99e7c2be 100644 --- a/tools/build-prod-env.js +++ b/tools/build-prod-env.js @@ -13,7 +13,7 @@ if (!_.isEqual(_.intersection(_.keys(argv), requiredParams), requiredParams)) { process.exit(2) } -fs.copyFileSync(path.resolve(__dirname, '../.sample.env'), path.resolve(__dirname, '../.env')) +fs.copyFileSync(path.resolve(__dirname, '../.sample.env'), process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env')) setEnvVariable('POSTGRES_USER', 'lamassu_pg') setEnvVariable('POSTGRES_PASSWORD', `${argv['db-password']}`) diff --git a/tools/set-env-var.js b/tools/set-env-var.js index 1b82cff0..fbe0086b 100644 --- a/tools/set-env-var.js +++ b/tools/set-env-var.js @@ -3,7 +3,8 @@ const os = require('os') const path = require('path') const setEnvVariable = (key, value) => { - const ENV_VARIABLES = fs.readFileSync(path.resolve(__dirname, '../.env'), 'utf-8').split(os.EOL) + const ENV_PATH = process.env.NODE_ENV === 'production' ? path.resolve(os.homedir(), '.lamassu', '.env') : path.resolve(__dirname, '../.env') + const ENV_VARIABLES = fs.readFileSync(ENV_PATH, 'utf-8').split(os.EOL) const target = ENV_VARIABLES.indexOf(ENV_VARIABLES.find(line => line.match(new RegExp(`^${key}=`)))) if (target < 0) { @@ -18,7 +19,7 @@ const setEnvVariable = (key, value) => { ENV_VARIABLES.splice(target, 1, `${key}=${value}`) } - fs.writeFileSync(path.resolve(__dirname, '../.env'), ENV_VARIABLES.join(os.EOL)) + fs.writeFileSync(ENV_PATH, ENV_VARIABLES.join(os.EOL)) } module.exports = setEnvVariable From ad5293522dc1cb30953a7f239e7316f2dfdf676e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 26 Apr 2022 10:06:36 +0100 Subject: [PATCH 21/44] refactor: use a more idiomatic conditional set --- lib/plugins/ticker/ccxt.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/plugins/ticker/ccxt.js b/lib/plugins/ticker/ccxt.js index 410b24fa..080b2f18 100644 --- a/lib/plugins/ticker/ccxt.js +++ b/lib/plugins/ticker/ccxt.js @@ -9,12 +9,14 @@ const RETRIES = 2 const tickerObjects = {} function ticker (fiatCode, cryptoCode, tickerName) { - const ticker = tickerObjects[tickerName] ? - tickerObjects[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) From 0a833fa11dba53f61143bcccf7be656d5a72e062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 16:58:52 +0100 Subject: [PATCH 22/44] refactor: remove old server --- lib/app.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/app.js b/lib/app.js index 72394605..7692ce2f 100644 --- a/lib/app.js +++ b/lib/app.js @@ -77,8 +77,6 @@ function startServer (settings) { : https.createServer(httpsServerOptions, routes.app) const port = argv.port || 3000 - const localPort = 3030 - const localServer = http.createServer(routes.localApp) if (options.devMode) logger.info('In dev mode') @@ -86,10 +84,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) - }) }) } From 5256c7ea4b4bd1899fed610dc9abc990d8c07734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 16:59:18 +0100 Subject: [PATCH 23/44] refactor: drop unnecessary imports --- lib/new-admin/admin-server.js | 3 +-- lib/new-config-manager.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 53f86ccc..550d71ed 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -8,13 +8,12 @@ const cors = require('cors') const helmet = require('helmet') const nocache = require('nocache') const cookieParser = require('cookie-parser') -const { ApolloServer, AuthenticationError } = require('apollo-server-express') +const { ApolloServer } = require('apollo-server-express') const { graphqlUploadExpress } = require('graphql-upload') const _ = require('lodash/fp') const { asyncLocalStorage, defaultStore } = require('../async-storage') const options = require('../options') -const users = require('../users') const logger = require('../logger') const { AuthDirective } = require('./graphql/directives') diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js index 4c05b540..8951b2a4 100644 --- a/lib/new-config-manager.js +++ b/lib/new-config-manager.js @@ -48,7 +48,6 @@ const getCommissions = (cryptoCode, deviceId, config) => { const getLocale = (deviceId, it) => { const locale = fromNamespace(namespaces.LOCALE)(it) - const filter = _.matches({ machine: deviceId }) return _.omit('overrides', _.assignAll([locale, ..._.filter(filter)(locale.overrides)])) } From 3fe217c5ed0d18dbb0854963538814fca07aef05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:01:49 +0100 Subject: [PATCH 24/44] refactor: drop unused constant --- lib/plugins.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/plugins.js b/lib/plugins.js index 80bbcc9b..3080f681 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -39,7 +39,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) { From 4a6da1b88884ffd6701cc5b60d51e9c790b3bf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:28:34 +0100 Subject: [PATCH 25/44] refactor: drop unused parameter --- lib/plugins.js | 2 +- lib/routes/pollingRoutes.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index 3080f681..c06da71a 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -227,7 +227,7 @@ function plugins (settings, deviceId) { } } - function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) { + function pollQueries (deviceTime, deviceRec, machineVersion, machineModel) { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index a2ca4847..b43008bb 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -61,7 +61,6 @@ function poll (req, res, next) { const machineModel = req.query.model const deviceId = req.deviceId const deviceTime = req.deviceTime - const serialNumber = req.query.sn const pid = req.query.pid const settings = req.settings const operatorId = res.locals.operatorId @@ -85,7 +84,7 @@ 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]) + return Promise.all([pi.pollQueries(deviceTime, req.query, machineVersion, machineModel), triggersPromise, triggersAutomationPromise]) .then(([results, triggers, triggersAutomation]) => { const cassettes = results.cassettes From 6a9643beb864f9dea8ee519f75124df293c41300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:30:18 +0100 Subject: [PATCH 26/44] fix: always run the `authorize` middleware --- lib/routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes.js b/lib/routes.js index 22b72bf2..6f727b02 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -56,7 +56,7 @@ 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) From b74bd2ce1424ef788e33edfab49fe58580a1553a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:31:28 +0100 Subject: [PATCH 27/44] refactor: remove unnecessary assignment --- lib/routes/pollingRoutes.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index b43008bb..2e470650 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -86,8 +86,6 @@ function poll (req, res, next) { return Promise.all([pi.pollQueries(deviceTime, req.query, machineVersion, machineModel), triggersPromise, triggersAutomationPromise]) .then(([results, triggers, triggersAutomation]) => { - const cassettes = results.cassettes - const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid const restartServices = pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid @@ -109,7 +107,6 @@ function poll (req, res, next) { receiptPrintingActive: receipt.active, smsReceiptActive: receipt.sms, enablePaperWalletOnly, - cassettes, twoWayMode: cashOutConfig.active, zeroConfLimits, reboot, From ac7cb243af33c46ce36872713b12e9632500f7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:31:52 +0100 Subject: [PATCH 28/44] refactor: use array destructuring instead of indexing --- lib/plugins.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index c06da71a..2a35b3cd 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -205,8 +205,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) From d2b78ea93d3862b0b8d5930e8a207296026c5672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:35:03 +0100 Subject: [PATCH 29/44] refactor: inline variables and reformat promises --- lib/routes/pollingRoutes.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index 2e470650..812b376f 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -72,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) @@ -84,7 +81,11 @@ function poll (req, res, next) { state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) - return Promise.all([pi.pollQueries(deviceTime, req.query, machineVersion, machineModel), triggersPromise, triggersAutomationPromise]) + return Promise.all([ + pi.pollQueries(deviceTime, req.query, machineVersion, machineModel), + buildTriggers(configManager.getTriggers(settings.config)), + configManager.getTriggersAutomation(settings.config) + ]) .then(([results, triggers, triggersAutomation]) => { const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid From af0029581e1502861a940482e2135b5c42514744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:42:02 +0100 Subject: [PATCH 30/44] refactor: no more array slicing in `pollQueries()` --- lib/plugins.js | 69 ++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index 2a35b3cd..fb05d049 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -230,52 +230,55 @@ function plugins (settings, deviceId) { 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 - ) + recordPing(deviceTime, machineVersion, machineModel), + fetchCurrentConfigVersion(), + millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)), + loyalty.getNumberOfAvailablePromoCodes(), + Promise.all(supportsBatchingPromise), + Promise.all(tickerPromises), + Promise.all(balancePromises), + Promise.all(networkPromises) + ]) + .then(([ + cassettes, + _pingRes, + 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 } }) } From 40791cb4cd1bd3b1c1653316c34e49e2176fa1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:44:04 +0100 Subject: [PATCH 31/44] refactor: move `recordPing()` to `poll()` --- lib/plugins.js | 5 ++--- lib/routes/pollingRoutes.js | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index fb05d049..ca07d516 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -226,7 +226,7 @@ function plugins (settings, deviceId) { } } - function pollQueries (deviceTime, deviceRec, machineVersion, machineModel) { + function pollQueries () { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies @@ -238,7 +238,6 @@ function plugins (settings, deviceId) { return Promise.all([ buildAvailableCassettes(), - recordPing(deviceTime, machineVersion, machineModel), fetchCurrentConfigVersion(), millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)), loyalty.getNumberOfAvailablePromoCodes(), @@ -249,7 +248,6 @@ function plugins (settings, deviceId) { ]) .then(([ cassettes, - _pingRes, configVersion, timezone, numberOfAvailablePromoCodes, @@ -851,6 +849,7 @@ function plugins (settings, deviceId) { return { getRates, + recordPing, buildRates, getRawRates, buildRatesNoCommission, diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index 812b376f..89e7aab1 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -82,11 +82,12 @@ function poll (req, res, next) { state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) return Promise.all([ - pi.pollQueries(deviceTime, req.query, machineVersion, machineModel), + pi.recordPing(deviceTime, machineVersion, machineModel), + pi.pollQueries(), buildTriggers(configManager.getTriggers(settings.config)), configManager.getTriggersAutomation(settings.config) ]) - .then(([results, triggers, triggersAutomation]) => { + .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 From 4d8c8c4b62ac4809c90408c7e05b94db7371a225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:44:54 +0100 Subject: [PATCH 32/44] fix: percentage division --- lib/plugins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugins.js b/lib/plugins.js index ca07d516..9030682f 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -364,12 +364,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, From a0ed5a3a0e0dd3bd3843ed93f4b72c792d5aedff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:47:01 +0100 Subject: [PATCH 33/44] feat: add GraphQL server and resolvers --- lib/graphql/resolvers.js | 140 +++++++++++++++++++++++++++++++++++++++ lib/graphql/server.js | 27 ++++++++ lib/graphql/types.js | 137 ++++++++++++++++++++++++++++++++++++++ lib/plugins.js | 77 +++++++++++++++++++++ lib/routes.js | 7 +- 5 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 lib/graphql/resolvers.js create mode 100644 lib/graphql/server.js create mode 100644 lib/graphql/types.js diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js new file mode 100644 index 00000000..62c24d4b --- /dev/null +++ b/lib/graphql/resolvers.js @@ -0,0 +1,140 @@ +const _ = require('lodash/fp') + +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 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] + }) +} + +const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, settings }, info) => + Promise.all([ + plugins(settings, deviceId).staticConfigQueries(), + !!configManager.getCompliance(settings.config).enablePaperWalletOnly, + configManager.getTriggersAutomation(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(([ + staticConf, + 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 = (parent, variables, { deviceId, operatorId, pid, settings }, info) => { + state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) + return plugins(settings, deviceId) + .dynamicConfigQueries() + .then(_.flow( + _.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), + )) +} + + +module.exports = { + Query: { + staticConfig, + dynamicConfig + } +} 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..1c8161ea --- /dev/null +++ b/lib/graphql/types.js @@ -0,0 +1,137 @@ +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 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 Query { + staticConfig(currentConfigVersion: Int): StaticConfig + dynamicConfig: DynamicConfig! +} +` diff --git a/lib/plugins.js b/lib/plugins.js index 9030682f..4f3f172c 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -25,6 +25,9 @@ const customers = require('./customers') const commissionMath = require('./commission-math') const loyalty = require('./loyalty') const transactionBatching = require('./tx-batching') +const state = require('./middlewares/state') + +const VERSION = require('../package.json').version const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants') @@ -281,6 +284,78 @@ function plugins (settings, deviceId) { }) } + function staticConfigQueries () { + const massageCoins = _.map(_.pick([ + 'batchable', + 'cashInCommission', + 'cashInFee', + 'cashOutCommission', + 'cryptoCode', + 'cryptoNetwork', + 'cryptoUnits', + 'display', + 'minimumTx' + ])) + + return pollQueries() + .then(_.flow( + _.pick([ + 'areThereAvailablePromoCodes', + 'coins', + 'configVersion', + 'timezone' + ]), + _.update('coins', massageCoins), + _.set('serverVersion', VERSION) + )) + } + + function dynamicConfigQueries () { + 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 + + return pollQueries() + .then(_.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)) + }) + )) + } + function sendCoins (tx) { return wallet.supportsBatching(settings, tx.cryptoCode) .then(supportsBatching => { @@ -854,6 +929,8 @@ function plugins (settings, deviceId) { getRawRates, buildRatesNoCommission, pollQueries, + staticConfigQueries, + dynamicConfigQueries, sendCoins, newAddress, isHd, diff --git a/lib/routes.js b/lib/routes.js index 6f727b02..5a608dd9 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -30,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 = [ @@ -39,7 +41,8 @@ const configRequiredRoutes = [ '/phone_code', '/customer', '/tx', - '/verify_promo_code' + '/verify_promo_code', + '/graphql' ] const devMode = argv.dev || options.http @@ -79,6 +82,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' }) From e30d3c035d666f5151665bcd85cb602721a611a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 5 Apr 2022 17:47:50 +0100 Subject: [PATCH 34/44] feat: add `recordPing()` as middleware to `/graphql` --- lib/middlewares/recordPing.js | 7 +++++++ lib/routes.js | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 lib/middlewares/recordPing.js 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/routes.js b/lib/routes.js index 5a608dd9..255490c8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -15,6 +15,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') @@ -64,6 +65,7 @@ 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) From b380e2845c41f3e6b4583f23247fdd5c62486eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 26 Apr 2022 12:52:04 +0100 Subject: [PATCH 35/44] chore: version bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5daa70b8..f5c222af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lamassu-server", - "version": "8.0.0-beta.4", + "version": "8.1.0-beta.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 539bdfaf..b4771d8a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "lamassu-server", "description": "bitcoin atm client server protocol module", "keywords": [], - "version": "8.0.0-beta.4", + "version": "8.1.0-beta.0", "license": "Unlicense", "author": "Lamassu (https://lamassu.is)", "dependencies": { From df2213566e7570f7249f3815421b32fc66290b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Mon, 11 Apr 2022 17:48:15 +0100 Subject: [PATCH 36/44] feat: add T&C configs to GraphQL API --- lib/graphql/resolvers.js | 16 ++++++++++++++++ lib/graphql/types.js | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 62c24d4b..e831f4d3 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -1,4 +1,5 @@ const _ = require('lodash/fp') +const nmd = require('nano-markdown') const { accounts: accountsConfig, countries, languages } = require('../new-admin/config') const plugins = require('../plugins') @@ -65,6 +66,18 @@ const buildTriggers = (allTriggers) => { }) } +/* + * 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 = (parent, { currentConfigVersion }, { deviceId, deviceName, settings }, info) => Promise.all([ plugins(settings, deviceId).staticConfigQueries(), @@ -75,6 +88,7 @@ const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, 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(([ @@ -86,6 +100,7 @@ const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, localeInfo, operatorInfo, receiptInfo, + terms, twoWayMode, ]) => (currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ? @@ -105,6 +120,7 @@ const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, twoWayMode, speedtestFiles, urlsToPing, + terms, }), _.update('triggersAutomation', _.mapValues(_.eq('Automatic'))), addOperatorInfo(operatorInfo), diff --git a/lib/graphql/types.js b/lib/graphql/types.js index 1c8161ea..7e504182 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -69,6 +69,14 @@ type Trigger { thresholdDays: Int } +type Terms { + delay: Boolean! + title: String! + text: String! + accept: String! + cancel: String! +} + type StaticConfig { configVersion: Int! @@ -90,6 +98,8 @@ type StaticConfig { triggersAutomation: TriggersAutomation! triggers: [Trigger!]! + + terms: Terms } type DynamicCoinValues { From d20f4968f650bebbc698bbd95747e3ab4cb22d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Fri, 22 Apr 2022 16:06:33 +0100 Subject: [PATCH 37/44] refactor: join static and dynamic configs to reuse `pollQueries()` --- lib/graphql/resolvers.js | 113 +++++++++++++++++++++++++++++++++------ lib/graphql/types.js | 8 ++- lib/plugins.js | 76 -------------------------- 3 files changed, 104 insertions(+), 93 deletions(-) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index e831f4d3..5d457c64 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -7,6 +7,8 @@ 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`, @@ -78,9 +80,31 @@ const massageTerms = terms => (terms.active && terms.text) ? ({ cancel: terms.cancelButtonText, }) : null -const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, settings }, info) => - Promise.all([ - plugins(settings, deviceId).staticConfigQueries(), +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(settings.config), buildTriggers(configManager.getTriggers(settings.config)), @@ -92,7 +116,6 @@ const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, !!configManager.getCashOut(deviceId, settings.config).active, ]) .then(([ - staticConf, enablePaperWalletOnly, triggersAutomation, triggers, @@ -126,6 +149,7 @@ const staticConfig = (parent, { currentConfigVersion }, { deviceId, deviceName, addOperatorInfo(operatorInfo), addReceiptInfo(receiptInfo) )(staticConf)) +} const setZeroConfLimit = config => coin => @@ -135,22 +159,81 @@ const setZeroConfLimit = config => coin => coin ) -const dynamicConfig = (parent, variables, { deviceId, operatorId, pid, settings }, info) => { +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 plugins(settings, deviceId) - .dynamicConfigQueries() - .then(_.flow( - _.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), - )) + + 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: { - staticConfig, - dynamicConfig + configs } } diff --git a/lib/graphql/types.js b/lib/graphql/types.js index 7e504182..a9318978 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -140,8 +140,12 @@ type DynamicConfig { restartServices: Boolean! } +type Configs { + static: StaticConfig + dynamic: DynamicConfig! +} + type Query { - staticConfig(currentConfigVersion: Int): StaticConfig - dynamicConfig: DynamicConfig! + configs(currentConfigVersion: Int): Configs! } ` diff --git a/lib/plugins.js b/lib/plugins.js index 4f3f172c..0fbff479 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -27,8 +27,6 @@ const loyalty = require('./loyalty') const transactionBatching = require('./tx-batching') const state = require('./middlewares/state') -const VERSION = require('../package.json').version - const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants') const notifier = require('./notifier') @@ -284,78 +282,6 @@ function plugins (settings, deviceId) { }) } - function staticConfigQueries () { - const massageCoins = _.map(_.pick([ - 'batchable', - 'cashInCommission', - 'cashInFee', - 'cashOutCommission', - 'cryptoCode', - 'cryptoNetwork', - 'cryptoUnits', - 'display', - 'minimumTx' - ])) - - return pollQueries() - .then(_.flow( - _.pick([ - 'areThereAvailablePromoCodes', - 'coins', - 'configVersion', - 'timezone' - ]), - _.update('coins', massageCoins), - _.set('serverVersion', VERSION) - )) - } - - function dynamicConfigQueries () { - 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 - - return pollQueries() - .then(_.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)) - }) - )) - } - function sendCoins (tx) { return wallet.supportsBatching(settings, tx.cryptoCode) .then(supportsBatching => { @@ -929,8 +855,6 @@ function plugins (settings, deviceId) { getRawRates, buildRatesNoCommission, pollQueries, - staticConfigQueries, - dynamicConfigQueries, sendCoins, newAddress, isHd, From fa2fe36c46f6042b0198a998ac9676c5ab617099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 26 Apr 2022 14:37:39 +0100 Subject: [PATCH 38/44] fix: add missing `getCustomInfoRequests()` call --- lib/graphql/resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 5d457c64..eb628fe7 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -106,7 +106,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings return Promise.all([ !!configManager.getCompliance(settings.config).enablePaperWalletOnly, - configManager.getTriggersAutomation(settings.config), + configManager.getTriggersAutomation(customRequestQueries.getCustomInfoRequests(), settings.config), buildTriggers(configManager.getTriggers(settings.config)), configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2', configManager.getLocale(deviceId, settings.config), From 6c43f7536d4835cdad10a7490a084c13db374a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 26 Apr 2022 15:23:42 +0100 Subject: [PATCH 39/44] refactor: move `getCustomInfoRequests` call out of config manager --- lib/graphql/resolvers.js | 6 +++--- lib/new-config-manager.js | 12 ++++++++---- lib/routes/pollingRoutes.js | 6 +++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index eb628fe7..01c2adb5 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -4,7 +4,7 @@ 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 { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests') const state = require('../middlewares/state') const VERSION = require('../../package.json').version @@ -56,7 +56,7 @@ const buildTriggers = (allTriggers) => { return _.flow( _.map(_.get('customInfoRequestId')), - customRequestQueries.batchGetCustomInfoRequest + batchGetCustomInfoRequest )(customTriggers) .then(res => { res.forEach((details, index) => { @@ -106,7 +106,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings return Promise.all([ !!configManager.getCompliance(settings.config).enablePaperWalletOnly, - configManager.getTriggersAutomation(customRequestQueries.getCustomInfoRequests(), settings.config), + configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config), buildTriggers(configManager.getTriggers(settings.config)), configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2', configManager.getLocale(deviceId, settings.config), diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js index 8951b2a4..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) @@ -116,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 = { @@ -154,6 +155,8 @@ const getCryptoUnits = (crypto, config) => { return getWalletSettings(crypto, config).cryptoUnits } +const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS) + module.exports = { getWalletSettings, getCashInSettings, @@ -173,5 +176,6 @@ module.exports = { getGlobalCashOut, getCashOut, getCryptosFromWalletNamespace, - getCryptoUnits + getCryptoUnits, + setTermsConditions, } diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index 89e7aab1..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 @@ -85,7 +85,7 @@ function poll (req, res, next) { pi.recordPing(deviceTime, machineVersion, machineModel), pi.pollQueries(), buildTriggers(configManager.getTriggers(settings.config)), - configManager.getTriggersAutomation(settings.config) + configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config), ]) .then(([_pingRes, results, triggers, triggersAutomation]) => { const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid From 7b951f961f22c7cdce4b63180d2f2722ff873d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 14 Apr 2022 12:01:20 +0100 Subject: [PATCH 40/44] feat: save T&C hash to the `user_config` --- lib/middlewares/populateDeviceId.js | 1 - lib/new-settings-loader.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) 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/new-settings-loader.js b/lib/new-settings-loader.js index 2f812c51..9dbba793 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,30 @@ const SECRET_FIELDS = [ 'twilio.authToken' ] +/* + * JSON.stringify isn't necessarily deterministic so this function may compute + * different hashes for the same object. + */ +const md5hash = obj => + crypto + .createHash('MD5') + .update(JSON.stringify(obj)) + .digest('hex') + +const addTermsHash = configs => { + configs = _.omit('termsConditions_hash', configs) + const terms = getTermsConditions(configs) + return _.isEmpty(terms) ? + configs : + _.flow( + _.omit(['hash']), + 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 +101,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 })])) From 469f38b7683d48b95805e0341f44f7839863f702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Tue, 26 Apr 2022 14:12:52 +0100 Subject: [PATCH 41/44] feat: put T&C in its own query --- lib/graphql/resolvers.js | 63 ++++++++++++++++++++++++++++++---------- lib/graphql/types.js | 10 +++++-- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 01c2adb5..2e0c68b0 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -68,18 +68,6 @@ const buildTriggers = (allTriggers) => { }) } -/* - * 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', @@ -112,7 +100,6 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings 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(([ @@ -123,7 +110,6 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings localeInfo, operatorInfo, receiptInfo, - terms, twoWayMode, ]) => (currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ? @@ -143,7 +129,6 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings twoWayMode, speedtestFiles, urlsToPing, - terms, }), _.update('triggersAutomation', _.mapValues(_.eq('Automatic'))), addOperatorInfo(operatorInfo), @@ -232,8 +217,54 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera }), })) + +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, { currentHash }, { settings }, info) => { + const isNone = x => _.isNil(x) || _.isEmpty(x) + + const latestTerms = configManager.getTermsConditions(settings.config) + if (isNone(latestTerms)) return null + + const hash = latestTerms.hash + if (!_.isString(hash)) return null + + if (hash === currentHash) return { hash, details: null } + + const details = massageTerms(latestTerms) + if (isNone(details)) return null + + return { hash, details } +} + + module.exports = { Query: { - configs + configs, + terms, } } diff --git a/lib/graphql/types.js b/lib/graphql/types.js index a9318978..ae4b24d3 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -69,7 +69,7 @@ type Trigger { thresholdDays: Int } -type Terms { +type TermsDetails { delay: Boolean! title: String! text: String! @@ -77,6 +77,11 @@ type Terms { cancel: String! } +type Terms { + hash: String! + details: TermsDetails +} + type StaticConfig { configVersion: Int! @@ -98,8 +103,6 @@ type StaticConfig { triggersAutomation: TriggersAutomation! triggers: [Trigger!]! - - terms: Terms } type DynamicCoinValues { @@ -147,5 +150,6 @@ type Configs { type Query { configs(currentConfigVersion: Int): Configs! + terms(currentHash: String): Terms } ` From 0213ceb7fe91fe3b35d0e6a29aa1f9e5de1b4d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 14 Apr 2022 16:59:16 +0100 Subject: [PATCH 42/44] feat: add DB migration to add the hash of T&C --- migrations/1649944954805-terms-and-conditions-hash.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 migrations/1649944954805-terms-and-conditions-hash.js 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() +} From c5e7627afb1895163752991289a6551904c07e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Wed, 27 Apr 2022 15:59:21 +0100 Subject: [PATCH 43/44] refactor: separate T&C text from rest of detials --- lib/graphql/resolvers.js | 18 ++++++++++++------ lib/graphql/types.js | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 2e0c68b0..ee1538f4 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -244,21 +244,27 @@ const massageTerms = terms => (terms.active && terms.text) ? ({ * `tc` is sent depending on whether the `hash` differs from `currentHash` or * not. */ -const terms = (parent, { currentHash }, { settings }, info) => { +const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settings }, info) => { const isNone = x => _.isNil(x) || _.isEmpty(x) - const latestTerms = configManager.getTermsConditions(settings.config) + let latestTerms = configManager.getTermsConditions(settings.config) if (isNone(latestTerms)) return null const hash = latestTerms.hash if (!_.isString(hash)) return null - if (hash === currentHash) return { hash, details: null } + latestTerms = massageTerms(latestTerms) + if (isNone(latestTerms)) return null - const details = massageTerms(latestTerms) - if (isNone(details)) return null + const isHashNew = hash !== currentHash + const text = isHashNew ? latestTerms.text : null - return { hash, details } + 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 })) } diff --git a/lib/graphql/types.js b/lib/graphql/types.js index ae4b24d3..04e2e69f 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -72,13 +72,13 @@ type Trigger { type TermsDetails { delay: Boolean! title: String! - text: String! accept: String! cancel: String! } type Terms { hash: String! + text: String details: TermsDetails } @@ -150,6 +150,6 @@ type Configs { type Query { configs(currentConfigVersion: Int): Configs! - terms(currentHash: String): Terms + terms(currentHash: String, currentConfigVersion: Int): Terms } ` From 028c8c3b1357b998d3f298d3d24aebaeb8c46e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Wed, 27 Apr 2022 16:00:08 +0100 Subject: [PATCH 44/44] fix: compute hash of the T&C text only --- lib/new-settings-loader.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js index 9dbba793..a9af0f30 100644 --- a/lib/new-settings-loader.js +++ b/lib/new-settings-loader.js @@ -30,19 +30,18 @@ const SECRET_FIELDS = [ * JSON.stringify isn't necessarily deterministic so this function may compute * different hashes for the same object. */ -const md5hash = obj => +const md5hash = text => crypto .createHash('MD5') - .update(JSON.stringify(obj)) + .update(text) .digest('hex') const addTermsHash = configs => { - configs = _.omit('termsConditions_hash', configs) - const terms = getTermsConditions(configs) + const terms = _.omit(['hash'], getTermsConditions(configs)) return _.isEmpty(terms) ? configs : _.flow( - _.omit(['hash']), + _.get('text'), md5hash, hash => _.set('hash', hash, terms), setTermsConditions,