From 68517170e2c01c3791987cd4d1e4f63405eb46cb Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Mon, 12 May 2025 15:35:00 +0100 Subject: [PATCH] chore: server code formatting --- eslint.config.mjs | 45 +- package-lock.json | 316 ++++++- package.json | 1 + packages/server/lib/app.js | 43 +- packages/server/lib/auth-tokens.js | 4 +- packages/server/lib/bill-math.js | 137 +-- packages/server/lib/blacklist.js | 20 +- packages/server/lib/blockchain/bitcoin.js | 84 +- packages/server/lib/blockchain/bitcoincash.js | 25 +- packages/server/lib/blockchain/common.js | 88 +- packages/server/lib/blockchain/dash.js | 49 +- packages/server/lib/blockchain/do-volume.js | 18 +- packages/server/lib/blockchain/ethereum.js | 13 +- packages/server/lib/blockchain/install.js | 199 ++-- packages/server/lib/blockchain/litecoin.js | 45 +- packages/server/lib/blockchain/monero.js | 19 +- packages/server/lib/blockchain/zcash.js | 25 +- .../lib/blockexplorers/mempool.space.js | 12 +- packages/server/lib/cash-in/cash-in-atomic.js | 66 +- packages/server/lib/cash-in/cash-in-low.js | 127 ++- packages/server/lib/cash-in/cash-in-tx.js | 154 ++-- .../server/lib/cash-out/cash-out-actions.js | 56 +- .../server/lib/cash-out/cash-out-atomic.js | 186 ++-- .../server/lib/cash-out/cash-out-helper.js | 108 ++- packages/server/lib/cash-out/cash-out-low.js | 64 +- packages/server/lib/cash-out/cash-out-tx.js | 149 +-- packages/server/lib/cashbox-batches.js | 155 ++-- packages/server/lib/coin-change.js | 34 +- .../server/lib/coinatmradar/coinatmradar.js | 81 +- .../coinatmradar/test/coinatmradar.test.js | 80 +- packages/server/lib/commission-math.js | 26 +- packages/server/lib/compliance-external.js | 39 +- packages/server/lib/compliance-triggers.js | 40 +- packages/server/lib/compliance.js | 100 +- packages/server/lib/compliance_overrides.js | 4 +- packages/server/lib/constants.js | 24 +- packages/server/lib/customer-notes.js | 6 +- packages/server/lib/customers.js | 742 ++++++++------- packages/server/lib/db-migrate-store.js | 8 +- packages/server/lib/db.js | 5 +- packages/server/lib/email.js | 34 +- packages/server/lib/environment-helper.js | 6 +- packages/server/lib/error.js | 2 +- packages/server/lib/event-bus.js | 13 +- packages/server/lib/exchange.js | 89 +- packages/server/lib/forex.js | 73 +- packages/server/lib/graphql/resolvers.js | 339 ++++--- packages/server/lib/graphql/server.js | 12 +- packages/server/lib/graphql/types.js | 388 ++++---- packages/server/lib/hardware-credentials.js | 12 +- packages/server/lib/layer2.js | 49 +- packages/server/lib/logger.js | 22 +- packages/server/lib/logs.js | 105 ++- packages/server/lib/loyalty.js | 38 +- packages/server/lib/machine-loader.js | 560 +++++++----- packages/server/lib/middlewares/authorize.js | 3 +- packages/server/lib/middlewares/ca.js | 5 +- .../server/lib/middlewares/errorHandler.js | 6 +- .../lib/middlewares/filterOldRequests.js | 12 +- packages/server/lib/middlewares/operatorId.js | 2 +- .../lib/middlewares/populateDeviceId.js | 11 +- .../lib/middlewares/populateSettings.js | 80 +- .../middlewares/rejectIncompatbleMachines.js | 14 +- packages/server/lib/middlewares/state.js | 6 +- packages/server/lib/migrate.js | 4 +- packages/server/lib/mnemonic-helpers.js | 4 +- packages/server/lib/new-admin/admin-server.js | 52 +- .../server/lib/new-admin/config/accounts.js | 197 +++- .../lib/new-admin/config/data/countries.json | 251 ++++- .../lib/new-admin/config/data/languages.json | 517 ++++++----- packages/server/lib/new-admin/config/index.js | 6 +- packages/server/lib/new-admin/filters.js | 4 +- .../lib/new-admin/graphql/directives/auth.js | 26 +- .../server/lib/new-admin/graphql/errors.js | 28 +- .../modules/authentication/FIDO2FAStrategy.js | 246 ++--- .../FIDOPasswordlessStrategy.js | 161 ++-- .../FIDOUsernamelessStrategy.js | 148 +-- .../graphql/modules/authentication/index.js | 4 +- .../graphql/modules/userManagement.js | 151 +-- .../graphql/resolvers/bill.resolver.js | 4 +- .../graphql/resolvers/blacklist.resolver.js | 6 +- .../graphql/resolvers/cashbox.resolver.js | 18 +- .../graphql/resolvers/config.resolver.js | 10 +- .../graphql/resolvers/currency.resolver.js | 4 +- .../resolvers/customInfoRequests.resolver.js | 51 +- .../graphql/resolvers/customer.resolver.js | 50 +- .../graphql/resolvers/funding.resolver.js | 4 +- .../lib/new-admin/graphql/resolvers/index.js | 2 +- .../graphql/resolvers/log.resolver.js | 21 +- .../graphql/resolvers/loyalty.resolver.js | 24 +- .../graphql/resolvers/machine.resolver.js | 20 +- .../graphql/resolvers/market.resolver.js | 4 +- .../resolvers/notification.resolver.js | 9 +- .../graphql/resolvers/pairing.resolver.js | 4 +- .../graphql/resolvers/rates.resolver.js | 6 +- .../graphql/resolvers/sanctions.resolver.js | 4 +- .../graphql/resolvers/scalar.resolver.js | 8 +- .../graphql/resolvers/settings.resolver.js | 7 +- .../graphql/resolvers/sms.resolver.js | 9 +- .../graphql/resolvers/status.resolver.js | 4 +- .../graphql/resolvers/transaction.resolver.js | 124 ++- .../graphql/resolvers/users.resolver.js | 144 ++- .../graphql/resolvers/version.resolver.js | 4 +- .../server/lib/new-admin/graphql/schema.js | 2 +- .../new-admin/graphql/types/cashbox.type.js | 6 +- .../graphql/types/customInfoRequests.type.js | 31 +- .../new-admin/graphql/types/customer.type.js | 29 +- .../lib/new-admin/graphql/types/index.js | 2 +- .../lib/new-admin/graphql/types/log.type.js | 32 +- .../new-admin/graphql/types/loyalty.type.js | 7 +- .../new-admin/graphql/types/machine.type.js | 9 +- .../lib/new-admin/graphql/types/sms.type.js | 3 +- .../graphql/types/transaction.type.js | 34 +- .../middlewares/cleanUserSessions.js | 8 +- .../lib/new-admin/middlewares/context.js | 5 +- .../server/lib/new-admin/middlewares/index.js | 2 +- .../lib/new-admin/middlewares/session.js | 33 +- .../server/lib/new-admin/services/bills.js | 16 +- .../new-admin/services/customInfoRequests.js | 105 ++- .../server/lib/new-admin/services/funding.js | 77 +- .../server/lib/new-admin/services/login.js | 17 +- .../server/lib/new-admin/services/machines.js | 17 +- .../server/lib/new-admin/services/pairing.js | 29 +- .../lib/new-admin/services/server-logs.js | 11 +- .../lib/new-admin/services/supervisor.js | 36 +- .../lib/new-admin/services/transactions.js | 298 ++++-- packages/server/lib/new-config-manager.js | 188 ++-- packages/server/lib/new-settings-loader.js | 197 ++-- packages/server/lib/notifier/codes.js | 6 +- packages/server/lib/notifier/email.js | 34 +- packages/server/lib/notifier/index.js | 304 ++++--- .../server/lib/notifier/notificationCenter.js | 211 +++-- packages/server/lib/notifier/queries.js | 23 +- packages/server/lib/notifier/sms.js | 16 +- .../server/lib/notifier/test/email.test.js | 12 +- .../server/lib/notifier/test/notifier.test.js | 108 ++- packages/server/lib/notifier/test/sms.test.js | 12 +- .../server/lib/notifier/test/utils.test.js | 52 +- packages/server/lib/notifier/utils.js | 79 +- packages/server/lib/notifier/webhook.js | 4 +- packages/server/lib/ofac/index.js | 41 +- packages/server/lib/ofac/loading.js | 51 +- packages/server/lib/ofac/matching.js | 53 +- packages/server/lib/ofac/name-utils.js | 15 +- packages/server/lib/ofac/parsing.js | 32 +- packages/server/lib/ofac/update.js | 52 +- packages/server/lib/operator.js | 5 +- packages/server/lib/pairing.js | 67 +- packages/server/lib/pg-transport.js | 6 +- packages/server/lib/plugin-helper.js | 9 +- packages/server/lib/plugins.js | 861 ++++++++++-------- packages/server/lib/plugins/common/ccxt.js | 104 +-- .../server/lib/plugins/common/json-rpc.js | 75 +- .../server/lib/plugins/compliance/consts.js | 4 +- .../mock-compliance/mock-compliance.js | 13 +- .../lib/plugins/compliance/sumsub/request.js | 11 +- .../plugins/compliance/sumsub/sumsub.api.js | 38 +- .../lib/plugins/compliance/sumsub/sumsub.js | 51 +- .../lib/plugins/email/mailgun/mailgun.js | 14 +- .../plugins/email/mock-email/mock-email.js | 4 +- .../server/lib/plugins/exchange/binance.js | 15 +- .../server/lib/plugins/exchange/binanceus.js | 15 +- .../server/lib/plugins/exchange/bitfinex.js | 16 +- .../server/lib/plugins/exchange/bitstamp.js | 54 +- packages/server/lib/plugins/exchange/ccxt.js | 221 +++-- packages/server/lib/plugins/exchange/cex.js | 15 +- .../server/lib/plugins/exchange/consts.js | 13 +- packages/server/lib/plugins/exchange/itbit.js | 67 +- .../server/lib/plugins/exchange/kraken.js | 86 +- .../lib/plugins/exchange/mock-exchange.js | 20 +- .../server/lib/plugins/sms/inforu/inforu.js | 93 +- .../lib/plugins/sms/mock-sms/mock-sms.js | 4 +- .../server/lib/plugins/sms/telnyx/telnyx.js | 11 +- .../server/lib/plugins/sms/twilio/twilio.js | 20 +- .../server/lib/plugins/sms/vonage/vonage.js | 11 +- .../lib/plugins/sms/whatsapp/whatsapp.js | 83 +- packages/server/lib/plugins/ticker/bitpay.js | 11 +- packages/server/lib/plugins/ticker/ccxt.js | 51 +- .../server/lib/plugins/ticker/mock-ticker.js | 8 +- .../ticker/pazuz-ticker/pazuz-ticker.js | 29 +- .../server/lib/plugins/tokens/erc20.abi.json | 316 ++++--- .../wallet-scoring/elliptic/elliptic.js | 56 +- .../mock-scoring/mock-scoring.js | 22 +- .../wallet-scoring/scorechain/scorechain.js | 31 +- .../wallet/bitcoincashd/bitcoincashd.js | 113 ++- .../lib/plugins/wallet/bitcoind/bitcoind.js | 180 ++-- .../server/lib/plugins/wallet/bitgo/bitgo.js | 117 +-- .../server/lib/plugins/wallet/dashd/dashd.js | 106 ++- .../server/lib/plugins/wallet/galoy/galoy.js | 323 ++++--- .../server/lib/plugins/wallet/geth/base.js | 216 +++-- .../server/lib/plugins/wallet/geth/geth.js | 2 +- .../lib/plugins/wallet/infura/infura.js | 49 +- .../lib/plugins/wallet/litecoind/litecoind.js | 97 +- .../plugins/wallet/mock-wallet/mock-wallet.js | 116 ++- .../lib/plugins/wallet/monerod/monerod.js | 247 +++-- .../wallet/pazuz-wallet/pazuz-wallet.js | 98 -- .../server/lib/plugins/wallet/tron/base.js | 102 ++- .../lib/plugins/wallet/trongrid/trongrid.js | 2 +- .../lib/plugins/wallet/zcashd/zcashd.js | 146 +-- .../zero-conf/blockcypher/blockcypher.js | 78 +- .../mock-zero-conf/mock-zero-conf.js | 16 +- packages/server/lib/poller.js | 207 +++-- packages/server/lib/postgresql_interface.js | 84 +- packages/server/lib/pp.js | 2 +- packages/server/lib/respond.js | 9 +- packages/server/lib/route-helpers.js | 34 +- packages/server/lib/routes.js | 17 +- packages/server/lib/routes/cashboxRoutes.js | 28 +- packages/server/lib/routes/customerRoutes.js | 269 ++++-- .../server/lib/routes/diagnosticsRoutes.js | 2 +- packages/server/lib/routes/failedQRScans.js | 2 +- packages/server/lib/routes/logsRoutes.js | 13 +- packages/server/lib/routes/pairingRoutes.js | 5 +- .../server/lib/routes/performanceRoutes.js | 9 +- packages/server/lib/routes/probeLnRoutes.js | 18 +- packages/server/lib/routes/stateRoutes.js | 5 +- .../lib/routes/termsAndConditionsRoutes.js | 27 +- packages/server/lib/routes/txRoutes.js | 23 +- .../lib/routes/verifyPromoCodeRoutes.js | 27 +- packages/server/lib/routes/verifyTxRoutes.js | 2 +- .../server/lib/routes/verifyUserRoutes.js | 2 +- packages/server/lib/sanctions.js | 42 +- packages/server/lib/session-manager.js | 21 +- packages/server/lib/sms-notices.js | 33 +- packages/server/lib/sms.js | 66 +- packages/server/lib/ticker.js | 44 +- packages/server/lib/time.js | 2 +- packages/server/lib/tx-batching-processing.js | 38 +- packages/server/lib/tx-batching.js | 58 +- packages/server/lib/tx.js | 71 +- packages/server/lib/users.js | 103 ++- packages/server/lib/utils.js | 3 +- packages/server/lib/wallet-scoring.js | 39 +- packages/server/lib/wallet.js | 259 ++++-- 234 files changed, 9824 insertions(+), 6195 deletions(-) delete mode 100644 packages/server/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 2447af6b..deb07af4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,13 +5,21 @@ import json from '@eslint/json' import { defineConfig, globalIgnores } from 'eslint/config' import reactCompiler from 'eslint-plugin-react-compiler' import eslintConfigPrettier from 'eslint-config-prettier/flat' +import pluginJest from 'eslint-plugin-jest' export default defineConfig([ - globalIgnores(['**/build', '**/package.json', '**/package-lock.json']), + globalIgnores([ + '**/build', + '**/package.json', + '**/package-lock.json', + '**/currencies.json', + '**/countries.json', + '**/languages.json', + ]), { files: ['**/*.{js,mjs,cjs,jsx}'], plugins: { js }, - extends: ['js/recommended'] + extends: ['js/recommended'], }, { files: ['packages/admin-ui/**/*.{js,mjs,jsx}'], @@ -19,18 +27,18 @@ export default defineConfig([ sourceType: 'module', globals: { ...globals.browser, - process: 'readonly' - } - } + process: 'readonly', + }, + }, }, { files: ['packages/server/**/*.{js,cjs}'], - languageOptions: { sourceType: 'commonjs', globals: globals.node } + languageOptions: { sourceType: 'commonjs', globals: globals.node }, }, { ...pluginReact.configs.flat.recommended, settings: { react: { version: 'detect' } }, - files: ['packages/admin-ui/**/*.{jsx,js}'] + files: ['packages/admin-ui/**/*.{jsx,js}'], }, { ...reactCompiler.configs.recommended }, eslintConfigPrettier, @@ -38,14 +46,29 @@ export default defineConfig([ files: ['**/*.json'], plugins: { json }, language: 'json/json', - extends: ['json/recommended'] + extends: ['json/recommended'], }, { rules: { 'react/prop-types': 'off', 'react/display-name': 'off', 'react/no-unescaped-entities': 'off', - 'react-compiler/react-compiler': 'warn' - } - } + 'react-compiler/react-compiler': 'warn', + }, + }, + { + // update this to match your test files + files: ['**/*.spec.js', '**/*.test.js'], + plugins: { jest: pluginJest }, + languageOptions: { + globals: pluginJest.environments.globals.globals, + }, + rules: { + 'jest/no-disabled-tests': 'warn', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + }, + }, ]) diff --git a/package-lock.json b/package-lock.json index a14199a4..04a689a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@eslint/json": "^0.12.0", "eslint": "^9.26.0", "eslint-config-prettier": "^10.1.5", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "^19.1.0-rc.1", "globals": "^16.1.0", @@ -5773,6 +5774,44 @@ } ] }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@otplib/core": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", @@ -7460,6 +7499,158 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", @@ -12177,6 +12368,32 @@ "eslint": ">=4.19.1" } }, + "node_modules/eslint-plugin-jest": { + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-node": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", @@ -13355,6 +13572,36 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -13375,6 +13622,16 @@ "resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.5.tgz", "integrity": "sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA==" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -14261,7 +14518,8 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/har-schema": { "version": "2.0.0", @@ -15331,6 +15589,7 @@ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "optional": true, + "peer": true, "bin": { "is-docker": "cli.js" }, @@ -15722,6 +15981,7 @@ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "optional": true, + "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -19561,6 +19821,16 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/merkle-lib": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", @@ -20233,6 +20503,7 @@ "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", "dev": true, "optional": true, + "peer": true, "dependencies": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -20248,6 +20519,7 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "optional": true, + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -22993,6 +23265,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -24036,7 +24319,8 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true, - "optional": true + "optional": true, + "peer": true }, "node_modules/side-channel": { "version": "1.1.0", @@ -26364,6 +26648,19 @@ "semver": "bin/semver" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", @@ -26615,6 +26912,21 @@ "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/ua-parser-js": { "version": "1.0.40", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", diff --git a/package.json b/package.json index a05fb6b9..78242888 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@eslint/json": "^0.12.0", "eslint": "^9.26.0", "eslint-config-prettier": "^10.1.5", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "^19.1.0-rc.1", "globals": "^16.1.0", diff --git a/packages/server/lib/app.js b/packages/server/lib/app.js index 875146b8..c3848f88 100644 --- a/packages/server/lib/app.js +++ b/packages/server/lib/app.js @@ -19,8 +19,8 @@ const CA_PATH = process.env.CA_PATH const version = require('../package.json').version logger.info('Version: %s', version) -function run () { - return new Promise((resolve, reject) => { +function run() { + return new Promise(resolve => { let count = 0 let handler @@ -31,12 +31,11 @@ function run () { } const runner = () => { - settingsLoader.loadLatest() + settingsLoader + .loadLatest() .then(settings => { clearInterval(handler) - return loadSanctions(settings) - .then(startServer) - .then(resolve) + return loadSanctions(settings).then(startServer).then(resolve) }) .catch(errorHandler) } @@ -46,23 +45,23 @@ function run () { }) } -function loadSanctions (settings) { - return Promise.resolve() - .then(() => { - const triggers = configManager.getTriggers(settings.config) - const hasSanctions = complianceTriggers.hasSanctions(triggers) +function loadSanctions(settings) { + return Promise.resolve().then(() => { + const triggers = configManager.getTriggers(settings.config) + const hasSanctions = complianceTriggers.hasSanctions(triggers) - if (!hasSanctions) return + if (!hasSanctions) return - logger.info('Loading sanctions DB...') - return ofacUpdate.update() - .then(() => logger.info('Sanctions DB updated')) - .then(ofac.load) - .then(() => logger.info('Sanctions DB loaded')) - }) + logger.info('Loading sanctions DB...') + return ofacUpdate + .update() + .then(() => logger.info('Sanctions DB updated')) + .then(ofac.load) + .then(() => logger.info('Sanctions DB loaded')) + }) } -async function startServer () { +async function startServer() { const app = await loadRoutes() poller.setup() @@ -72,16 +71,14 @@ async function startServer () { cert: fs.readFileSync(CERT_PATH), ca: fs.readFileSync(CA_PATH), requestCert: true, - rejectUnauthorized: false + rejectUnauthorized: false, } const server = https.createServer(httpsServerOptions, app) const port = argv.port || 3000 - await new Promise((resolve) => - server.listen({ port }, resolve), - ) + await new Promise(resolve => server.listen({ port }, resolve)) logger.info(`lamassu-server listening on port ${port}`) } diff --git a/packages/server/lib/auth-tokens.js b/packages/server/lib/auth-tokens.js index d42fa1b1..6fd8920a 100644 --- a/packages/server/lib/auth-tokens.js +++ b/packages/server/lib/auth-tokens.js @@ -3,7 +3,7 @@ const crypto = require('crypto') const constants = require('./constants') const db = require('./db') -function createAuthToken (userID, type) { +function createAuthToken(userID, type) { const token = crypto.randomBytes(32).toString('hex') const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '${constants.AUTH_TOKEN_EXPIRATION_TIME}' RETURNING *` @@ -11,5 +11,5 @@ function createAuthToken (userID, type) { } module.exports = { - createAuthToken + createAuthToken, } diff --git a/packages/server/lib/bill-math.js b/packages/server/lib/bill-math.js index f83ff49c..570c998d 100644 --- a/packages/server/lib/bill-math.js +++ b/packages/server/lib/bill-math.js @@ -1,7 +1,6 @@ const _ = require('lodash/fp') const sumService = require('@haensl/subset-sum') -const BN = require('./bn') const logger = require('./logger') const cc = require('./coin-change') @@ -11,7 +10,7 @@ const BILL_LIST_MODES = { LOWEST_VALUE_FIRST: 2, HIGHEST_VALUE_FIRST: 3, UNIT_ROUND_ROBIN: 4, - VALUE_ROUND_ROBIN: 5 + VALUE_ROUND_ROBIN: 5, } const buildBillList = (units, mode) => { @@ -23,7 +22,7 @@ const buildBillList = (units, mode) => { return acc }, [], - _.reverse(units) + _.reverse(units), ) case BILL_LIST_MODES.FIRST_UNIT_FIRST: return _.reduce( @@ -32,7 +31,7 @@ const buildBillList = (units, mode) => { return acc }, [], - units + units, ) case BILL_LIST_MODES.LOWEST_VALUE_FIRST: return _.reduce( @@ -41,7 +40,7 @@ const buildBillList = (units, mode) => { return acc }, [], - _.orderBy(['denomination'], ['asc'])(units) + _.orderBy(['denomination'], ['asc'])(units), ) case BILL_LIST_MODES.HIGHEST_VALUE_FIRST: return _.reduce( @@ -50,58 +49,59 @@ const buildBillList = (units, mode) => { return acc }, [], - _.orderBy(['denomination'], ['desc'])(units) + _.orderBy(['denomination'], ['desc'])(units), ) - case BILL_LIST_MODES.UNIT_ROUND_ROBIN: - { - const amountOfBills = _.reduce( - (acc, value) => acc + value.count, - 0, - units - ) - - const _units = _.filter(it => it.count > 0)(_.cloneDeep(units)) - const bills = [] - - for(let i = 0; i < amountOfBills; i++) { - const idx = i % _.size(_units) - if (_units[idx].count > 0) { - bills.push(_units[idx].denomination) - _units[idx].count-- - } - - if (_units[idx].count === 0) { - _units.splice(idx, 1) - } + case BILL_LIST_MODES.UNIT_ROUND_ROBIN: { + const amountOfBills = _.reduce( + (acc, value) => acc + value.count, + 0, + units, + ) + + const _units = _.filter(it => it.count > 0)(_.cloneDeep(units)) + const bills = [] + + for (let i = 0; i < amountOfBills; i++) { + const idx = i % _.size(_units) + if (_units[idx].count > 0) { + bills.push(_units[idx].denomination) + _units[idx].count-- } - return bills + if (_units[idx].count === 0) { + _units.splice(idx, 1) + } } - case BILL_LIST_MODES.VALUE_ROUND_ROBIN: - { - const amountOfBills = _.reduce( - (acc, value) => acc + value.count, - 0, - units - ) - - const _units = _.flow([_.filter(it => it.count > 0), _.orderBy(['denomination'], ['asc'])])(_.cloneDeep(units)) - const bills = [] - - for(let i = 0; i < amountOfBills; i++) { - const idx = i % _.size(_units) - if (_units[idx].count > 0) { - bills.push(_units[idx].denomination) - _units[idx].count-- - } - - if (_units[idx].count === 0) { - _units.splice(idx, 1) - } + + return bills + } + case BILL_LIST_MODES.VALUE_ROUND_ROBIN: { + const amountOfBills = _.reduce( + (acc, value) => acc + value.count, + 0, + units, + ) + + const _units = _.flow([ + _.filter(it => it.count > 0), + _.orderBy(['denomination'], ['asc']), + ])(_.cloneDeep(units)) + const bills = [] + + for (let i = 0; i < amountOfBills; i++) { + const idx = i % _.size(_units) + if (_units[idx].count > 0) { + bills.push(_units[idx].denomination) + _units[idx].count-- } - return bills + if (_units[idx].count === 0) { + _units.splice(idx, 1) + } } + + return bills + } default: throw new Error(`Invalid mode: ${mode}`) } @@ -113,11 +113,13 @@ const getSolution_old = (units, amount, mode) => { if (_.sum(billList) < amount.toNumber()) { return [] } - + const solver = sumService.subsetSum(billList, amount.toNumber()) const solution = _.countBy(Math.floor, solver.next().value) - return Object.entries(solution) - .map(([denomination, provisioned]) => [_.toNumber(denomination), provisioned]) + return Object.entries(solution).map(([denomination, provisioned]) => [ + _.toNumber(denomination), + provisioned, + ]) } const getSolution = (units, amount) => { @@ -128,28 +130,35 @@ const getSolution = (units, amount) => { } const solutionToOriginalUnits = (solution, units) => { - const billsToAssign = (count, left) => _.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left) + const billsToAssign = (count, left) => + _.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left) const billsLeft = Object.fromEntries(solution) - return units.map( - ({ count, name, denomination }) => { - const provisioned = billsToAssign(count, billsLeft[denomination]) - billsLeft[denomination] -= provisioned - return { name, denomination, provisioned } - } - ) + return units.map(({ count, name, denomination }) => { + const provisioned = billsToAssign(count, billsLeft[denomination]) + billsLeft[denomination] -= provisioned + return { name, denomination, provisioned } + }) } function makeChange(outCassettes, amount) { - const ss_solution = getSolution_old(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN) + const ss_solution = getSolution_old( + outCassettes, + amount, + BILL_LIST_MODES.VALUE_ROUND_ROBIN, + ) const cc_solution = getSolution(outCassettes, amount) if (!cc.check(cc_solution, amount.toNumber())) { - logger.error(new Error("coin-change provided a bad solution")) + logger.error(new Error('coin-change provided a bad solution')) return solutionToOriginalUnits(ss_solution, outCassettes) } if (!!ss_solution !== !!cc_solution) { - logger.error(new Error(`subset-sum and coin-change don't agree on solvability -- subset-sum:${!!ss_solution} coin-change:${!!cc_solution}`)) + logger.error( + new Error( + `subset-sum and coin-change don't agree on solvability -- subset-sum:${!!ss_solution} coin-change:${!!cc_solution}`, + ), + ) return solutionToOriginalUnits(ss_solution, outCassettes) } diff --git a/packages/server/lib/blacklist.js b/packages/server/lib/blacklist.js index 7665916c..b4dbc12f 100644 --- a/packages/server/lib/blacklist.js +++ b/packages/server/lib/blacklist.js @@ -8,7 +8,7 @@ const getBlacklist = () => db.any( `SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage FROM blacklist JOIN blacklist_messages - ON blacklist.blacklist_message_id = blacklist_messages.id` + ON blacklist.blacklist_message_id = blacklist_messages.id`, ) const deleteFromBlacklist = address => { @@ -19,7 +19,9 @@ const deleteFromBlacklist = address => { const isValidAddress = address => { try { - return !_.isEmpty(addressDetector.getSupportedCoinsForAddress(address).matches) + return !_.isEmpty( + addressDetector.getSupportedCoinsForAddress(address).matches, + ) } catch { return false } @@ -29,24 +31,20 @@ const insertIntoBlacklist = address => { if (!isValidAddress(address)) { return Promise.reject(new Error('Invalid address')) } - return db - .none( - 'INSERT INTO blacklist (address) VALUES ($1);', - [address] - ) + return db.none('INSERT INTO blacklist (address) VALUES ($1);', [address]) } -function blocked (address) { +function blocked(address) { const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1` return db.oneOrNone(sql, [address]) } -function getMessages () { +function getMessages() { const sql = `SELECT * FROM blacklist_messages` return db.any(sql) } -function editBlacklistMessage (id, content) { +function editBlacklistMessage(id, content) { const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id` return db.oneOrNone(sql, [content, id]) } @@ -57,5 +55,5 @@ module.exports = { deleteFromBlacklist, insertIntoBlacklist, getMessages, - editBlacklistMessage + editBlacklistMessage, } diff --git a/packages/server/lib/blockchain/bitcoin.js b/packages/server/lib/blockchain/bitcoin.js index 9d91ecb9..d066c633 100644 --- a/packages/server/lib/blockchain/bitcoin.js +++ b/packages/server/lib/blockchain/bitcoin.js @@ -1,5 +1,4 @@ const path = require('path') -const _ = require('lodash/fp') const { utils: coinUtils } = require('@lamassu/coins') @@ -13,9 +12,11 @@ const coinRec = coinUtils.getCryptoCurrency('BTC') const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp') : '/tmp' -const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin' +const usrBinDir = isDevMode() + ? path.resolve(BLOCKCHAIN_DIR, 'bin') + : '/usr/local/bin' -function setup (dataDir) { +function setup(dataDir) { !isDevMode() && common.firewall([coinRec.defaultPort]) const config = buildConfig() common.writeFile(path.resolve(dataDir, coinRec.configFile), config) @@ -23,12 +24,17 @@ function setup (dataDir) { !isDevMode() && common.writeSupervisorConfig(coinRec, cmd) } -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating Bitcoin Core. This may take a minute...') !isDevMode() && common.es(`sudo supervisorctl stop bitcoin`) common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Bitcoin Core: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Bitcoin Core: Package signature do not match!', + ) return } common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`) @@ -38,39 +44,71 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`rm -r ${tmpDir}/${coinRec.dir.replace('/bin', '')}`) common.es(`rm ${tmpDir}/bitcoin.tar.gz`) - if (common.es(`grep "addresstype=p2sh-segwit" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + if ( + common.es( + `grep "addresstype=p2sh-segwit" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`, + ) + ) { common.logger.info(`Enabling bech32 receiving addresses in config file..`) - common.es(`sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + common.es( + `sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`, + ) } else { - common.logger.info(`bech32 receiving addresses already defined, skipping...`) + common.logger.info( + `bech32 receiving addresses already defined, skipping...`, + ) } - if (common.es(`grep "changetype=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + if ( + common.es( + `grep "changetype=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`, + ) + ) { common.logger.info(`changetype already defined, skipping...`) } else { common.logger.info(`Enabling bech32 change addresses in config file..`) - common.es(`echo "\nchangetype=bech32" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + common.es( + `echo "\nchangetype=bech32" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`, + ) } - if (common.es(`grep "listenonion=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + if ( + common.es( + `grep "listenonion=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`, + ) + ) { common.logger.info(`listenonion already defined, skipping...`) } else { common.logger.info(`Setting 'listenonion=0' in config file...`) - common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + common.es( + `echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`, + ) } - if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + if ( + common.es( + `grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`, + ) + ) { common.logger.info(`fallbackfee already defined, skipping...`) } else { common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`) - common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + common.es( + `echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`, + ) } - if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + if ( + common.es( + `grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`, + ) + ) { common.logger.info(`rpcworkqueue already defined, skipping...`) } else { common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`) - common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + common.es( + `echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`, + ) } if (isCurrentlyRunning && !isDevMode()) { @@ -81,7 +119,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Bitcoin Core is updated!') } -function buildConfig () { +function buildConfig() { return `rpcuser=lamassuserver rpcpassword=${common.randomPass()} ${isDevMode() ? `regtest=1` : ``} @@ -97,13 +135,15 @@ walletrbf=1 listenonion=0 fallbackfee=0.00005 rpcworkqueue=2000 -${isDevMode() - ? `[regtest] +${ + isDevMode() + ? `[regtest] rpcport=18333 bind=0.0.0.0:18332 ${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}` - : `rpcport=8333 + : `rpcport=8333 bind=0.0.0.0:8332 -${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`} +${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}` +} ` } diff --git a/packages/server/lib/blockchain/bitcoincash.js b/packages/server/lib/blockchain/bitcoincash.js index 53e4ab8e..4a4d891b 100644 --- a/packages/server/lib/blockchain/bitcoincash.js +++ b/packages/server/lib/blockchain/bitcoincash.js @@ -8,7 +8,7 @@ module.exports = { setup, updateCore } const coinRec = coinUtils.getCryptoCurrency('BCH') -function setup (dataDir) { +function setup(dataDir) { common.firewall([coinRec.defaultPort]) const config = buildConfig() common.writeFile(path.resolve(dataDir, coinRec.configFile), config) @@ -16,12 +16,17 @@ function setup (dataDir) { common.writeSupervisorConfig(coinRec, cmd) } -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating Bitcoin Cash. This may take a minute...') common.es(`sudo supervisorctl stop bitcoincash`) common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Bitcoin Cash: Package signature do not match!', + ) return } common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`) @@ -32,11 +37,17 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`) common.es(`rm /tmp/bitcoincash.tar.gz`) - if (common.es(`grep "listenonion=" /mnt/blockchains/bitcoincash/bitcoincash.conf || true`)) { + if ( + common.es( + `grep "listenonion=" /mnt/blockchains/bitcoincash/bitcoincash.conf || true`, + ) + ) { common.logger.info(`listenonion already defined, skipping...`) } else { common.logger.info(`Setting 'listenonion=0' in config file...`) - common.es(`echo "\nlistenonion=0" >> /mnt/blockchains/bitcoincash/bitcoincash.conf`) + common.es( + `echo "\nlistenonion=0" >> /mnt/blockchains/bitcoincash/bitcoincash.conf`, + ) } if (isCurrentlyRunning) { @@ -47,7 +58,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Bitcoin Cash is updated!') } -function buildConfig () { +function buildConfig() { return `rpcuser=lamassuserver rpcpassword=${common.randomPass()} dbcache=500 diff --git a/packages/server/lib/blockchain/common.js b/packages/server/lib/blockchain/common.js index 169deb45..e9b105bb 100644 --- a/packages/server/lib/blockchain/common.js +++ b/packages/server/lib/blockchain/common.js @@ -7,7 +7,7 @@ const makeDir = require('make-dir') const _ = require('lodash/fp') -const logger = require('console-log-level')({level: 'info'}) +const logger = require('console-log-level')({ level: 'info' }) const { isDevMode } = require('../environment-helper') @@ -23,13 +23,15 @@ module.exports = { isInstalledSoftware, writeFile, getBinaries, - isUpdateDependent + isUpdateDependent, } const BINARIES = { BTC: { - defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', - defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397', + defaultUrl: + 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', + defaultUrlHash: + '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397', defaultDir: 'bitcoin-0.20.1/bin', url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz', dir: 'bitcoin-28.0/bin', @@ -46,16 +48,20 @@ const BINARIES = { urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d', }, DASH: { - defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', - defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219', + defaultUrl: + 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', + defaultUrlHash: + 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219', defaultDir: 'dashcore-18.1.0/bin', url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz', dir: 'dashcore-21.1.1/bin', urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5', }, LTC: { - defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', - defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751', + defaultUrl: + 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', + defaultUrlHash: + 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751', defaultDir: 'litecoin-0.18.1/bin', url: 'https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz', dir: 'litecoin-0.21.4/bin', @@ -64,38 +70,44 @@ const BINARIES = { BCH: { url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.0/bitcoin-cash-node-28.0.0-x86_64-linux-gnu.tar.gz', dir: 'bitcoin-cash-node-28.0.0/bin', - files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']], + files: [ + ['bitcoind', 'bitcoincashd'], + ['bitcoin-cli', 'bitcoincash-cli'], + ], urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158', }, XMR: { url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2', dir: 'monero-x86_64-linux-gnu-v0.18.3.4', - files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']], + files: [ + ['monerod', 'monerod'], + ['monero-wallet-rpc', 'monero-wallet-rpc'], + ], urlHash: '51ba03928d189c1c11b5379cab17dd9ae8d2230056dc05c872d0f8dba4a87f1d', - } + }, } const coinsUpdateDependent = ['BTC', 'LTC', 'DASH'] -function firewall (ports) { +function firewall(ports) { if (!ports || ports.length === 0) throw new Error('No ports supplied') const portsString = ports.join(',') es(`sudo ufw allow ${portsString}`) } -function randomPass () { +function randomPass() { return crypto.randomBytes(32).toString('hex') } -function es (cmd) { - const env = {HOME: os.userInfo().homedir} - const options = {encoding: 'utf8', env} +function es(cmd) { + const env = { HOME: os.userInfo().homedir } + const options = { encoding: 'utf8', env } const res = cp.execSync(cmd, options) logger.debug(res) return res.toString() } -function generateSupervisorConfig (cryptoCode, command, isWallet = false) { +function generateSupervisorConfig(cryptoCode, command, isWallet = false) { return `[program:${cryptoCode}${isWallet ? `-wallet` : ``}] command=nice ${command} autostart=true @@ -108,34 +120,46 @@ environment=HOME="/root" ` } -function writeSupervisorConfig (coinRec, cmd, walletCmd = '') { +function writeSupervisorConfig(coinRec, cmd, walletCmd = '') { if (isInstalledSoftware(coinRec)) return const blockchain = coinRec.code if (!_.isNil(coinRec.wallet)) { - const supervisorConfigWallet = generateSupervisorConfig(blockchain, walletCmd, true) - writeFile(`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`, supervisorConfigWallet) + const supervisorConfigWallet = generateSupervisorConfig( + blockchain, + walletCmd, + true, + ) + writeFile( + `/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`, + supervisorConfigWallet, + ) } const supervisorConfig = generateSupervisorConfig(blockchain, cmd) writeFile(`/etc/supervisor/conf.d/${coinRec.code}.conf`, supervisorConfig) } -function isInstalledSoftware (coinRec) { +function isInstalledSoftware(coinRec) { if (isDevMode()) { - return fs.existsSync(`${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`) - && fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`) + return ( + fs.existsSync( + `${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`, + ) && fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`) + ) } - const nodeInstalled = fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`) + const nodeInstalled = fs.existsSync( + `/etc/supervisor/conf.d/${coinRec.code}.conf`, + ) const walletInstalled = _.isNil(coinRec.wallet) ? true : fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.wallet.conf`) return nodeInstalled && walletInstalled } -function fetchAndInstall (coinRec) { +function fetchAndInstall(coinRec) { const requiresUpdate = isUpdateDependent(coinRec.cryptoCode) if (isInstalledSoftware(coinRec)) return @@ -149,12 +173,16 @@ function fetchAndInstall (coinRec) { es(`wget -q ${url}`) if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) { - logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`) + logger.info( + `Failed to install ${coinRec.code}: Package signature do not match!`, + ) return } es(`tar -xf ${downloadFile}`) - const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin' + const usrBinDir = isDevMode() + ? path.resolve(BLOCKCHAIN_DIR, 'bin') + : '/usr/local/bin' if (isDevMode()) { makeDir.sync(usrBinDir) @@ -170,7 +198,7 @@ function fetchAndInstall (coinRec) { }, binaries.files) } -function writeFile (path, content) { +function writeFile(path, content) { try { fs.writeFileSync(path, content) } catch (err) { @@ -183,12 +211,12 @@ function writeFile (path, content) { } } -function getBinaries (coinCode) { +function getBinaries(coinCode) { const binaries = BINARIES[coinCode] if (!binaries) throw new Error(`No such coin: ${coinCode}`) return binaries } -function isUpdateDependent (coinCode) { +function isUpdateDependent(coinCode) { return _.includes(coinCode, coinsUpdateDependent) } diff --git a/packages/server/lib/blockchain/dash.js b/packages/server/lib/blockchain/dash.js index 51ed159f..29a74088 100644 --- a/packages/server/lib/blockchain/dash.js +++ b/packages/server/lib/blockchain/dash.js @@ -8,7 +8,7 @@ module.exports = { setup, updateCore } const coinRec = coinUtils.getCryptoCurrency('DASH') -function setup (dataDir) { +function setup(dataDir) { common.firewall([coinRec.defaultPort]) const config = buildConfig() common.writeFile(path.resolve(dataDir, coinRec.configFile), config) @@ -16,12 +16,17 @@ function setup (dataDir) { common.writeSupervisorConfig(coinRec, cmd) } -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating Dash Core. This may take a minute...') common.es(`sudo supervisorctl stop dash`) common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Dash Core: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Dash Core: Package signature do not match!', + ) return } common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`) @@ -31,20 +36,38 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`) common.es(`rm /tmp/dash.tar.gz`) - if (common.es(`grep "enableprivatesend=" /mnt/blockchains/dash/dash.conf || true`)) { + if ( + common.es( + `grep "enableprivatesend=" /mnt/blockchains/dash/dash.conf || true`, + ) + ) { common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`) - common.es(`sed -i 's/enableprivatesend/enablecoinjoin/g' /mnt/blockchains/dash/dash.conf`) - } else if (common.es(`grep "enablecoinjoin=" /mnt/blockchains/dash/dash.conf || true`)) { + common.es( + `sed -i 's/enableprivatesend/enablecoinjoin/g' /mnt/blockchains/dash/dash.conf`, + ) + } else if ( + common.es(`grep "enablecoinjoin=" /mnt/blockchains/dash/dash.conf || true`) + ) { common.logger.info(`enablecoinjoin already defined, skipping...`) } else { common.logger.info(`Enabling CoinJoin in config file...`) common.es(`echo "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`) } - if (common.es(`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`)) { + if ( + common.es( + `grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`, + ) + ) { common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`) - common.es(`sed -i 's/privatesendautostart/coinjoinautostart/g' /mnt/blockchains/dash/dash.conf`) - } else if (common.es(`grep "coinjoinautostart=" /mnt/blockchains/dash/dash.conf || true`)) { + common.es( + `sed -i 's/privatesendautostart/coinjoinautostart/g' /mnt/blockchains/dash/dash.conf`, + ) + } else if ( + common.es( + `grep "coinjoinautostart=" /mnt/blockchains/dash/dash.conf || true`, + ) + ) { common.logger.info(`coinjoinautostart already defined, skipping...`) } else { common.logger.info(`Enabling CoinJoin AutoStart in config file...`) @@ -53,7 +76,9 @@ function updateCore (coinRec, isCurrentlyRunning) { 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`) + common.es( + `sed -i 's/litemode/disablegovernance/g' /mnt/blockchains/dash/dash.conf`, + ) } else { common.es(`echo "\ndisablegovernance already defined, skipping..."`) } @@ -66,7 +91,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Dash Core is updated!') } -function buildConfig () { +function buildConfig() { return `rpcuser=lamassuserver rpcpassword=${common.randomPass()} dbcache=500 diff --git a/packages/server/lib/blockchain/do-volume.js b/packages/server/lib/blockchain/do-volume.js index e3f5b825..671d8d68 100644 --- a/packages/server/lib/blockchain/do-volume.js +++ b/packages/server/lib/blockchain/do-volume.js @@ -6,20 +6,20 @@ const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR const MOUNT_POINT = BLOCKCHAIN_DIR -module.exports = {prepareVolume} +module.exports = { prepareVolume } const logger = common.logger -function isMounted () { +function isMounted() { return fs.existsSync(MOUNT_POINT) } -function isFormatted (volumePath) { +function isFormatted(volumePath) { const res = common.es(`file --dereference -s ${volumePath}`).trim() return res !== `${volumePath}: data` } -function formatVolume (volumePath) { +function formatVolume(volumePath) { if (isFormatted(volumePath)) { logger.info('Volume is already formatted.') return @@ -29,7 +29,7 @@ function formatVolume (volumePath) { common.es(`sudo mkfs.ext4 ${volumePath}`) } -function mountVolume (volumePath) { +function mountVolume(volumePath) { if (isMounted()) { logger.info('Volume is already mounted.') return @@ -38,10 +38,12 @@ function mountVolume (volumePath) { logger.info('Mounting...') common.es(`sudo mkdir -p ${MOUNT_POINT}`) common.es(`sudo mount -o discard,defaults ${volumePath} ${MOUNT_POINT}`) - common.es(`echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`) + common.es( + `echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`, + ) } -function locateVolume () { +function locateVolume() { const res = common.es('ls /dev/disk/by-id/*') const lines = res.trim().split('\n') @@ -58,7 +60,7 @@ function locateVolume () { return lines[0].trim() } -function prepareVolume () { +function prepareVolume() { if (isMounted()) { logger.info('Volume is already mounted.') return true diff --git a/packages/server/lib/blockchain/ethereum.js b/packages/server/lib/blockchain/ethereum.js index 9434ebdc..42eab589 100644 --- a/packages/server/lib/blockchain/ethereum.js +++ b/packages/server/lib/blockchain/ethereum.js @@ -4,11 +4,16 @@ const common = require('./common') module.exports = { setup, updateCore } -function updateCore (coinRec, isCurrentlyRunning) { - common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...') +function updateCore(coinRec, isCurrentlyRunning) { + common.logger.info( + 'Updating the Geth Ethereum wallet. This may take a minute...', + ) common.es(`sudo supervisorctl stop ethereum`) common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + if ( + common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { common.logger.info('Failed to update Geth: Package signature do not match!') return } @@ -27,7 +32,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Geth is updated!') } -function setup (dataDir) { +function setup(dataDir) { const coinRec = coinUtils.getCryptoCurrency('ETH') common.firewall([coinRec.defaultPort]) const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --http` diff --git a/packages/server/lib/blockchain/install.js b/packages/server/lib/blockchain/install.js index 821591d6..5aa2f744 100644 --- a/packages/server/lib/blockchain/install.js +++ b/packages/server/lib/blockchain/install.js @@ -10,7 +10,11 @@ const _ = require('lodash/fp') const { utils: coinUtils } = require('@lamassu/coins') const settingsLoader = require('../new-settings-loader') const wallet = require('../wallet') -const { isDevMode, isRemoteNode, isRemoteWallet } = require('../environment-helper') +const { + isDevMode, + isRemoteNode, + isRemoteWallet, +} = require('../environment-helper') const common = require('./common') const doVolume = require('./do-volume') @@ -24,35 +28,38 @@ const PLUGINS = { BCH: require('./bitcoincash.js'), DASH: require('./dash.js'), LTC: require('./litecoin.js'), - XMR: require('./monero.js') + XMR: require('./monero.js'), } const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR module.exports = { isEnvironmentValid, - run + run, } -function installedVolumeFilePath (crypto) { +function installedVolumeFilePath(crypto) { return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed') } -function isInstalledVolume (crypto) { +function isInstalledVolume(crypto) { return fs.existsSync(installedVolumeFilePath(crypto)) } -function isInstalledSoftware (crypto) { +function isInstalledSoftware(crypto) { return common.isInstalledSoftware(crypto) } -function processCryptos (codes) { +function processCryptos(codes) { if (_.isEmpty(codes)) { logger.info('No cryptos selected. Exiting.') process.exit(0) } - logger.info('Thanks! Installing: %s. Will take a while...', _.join(', ', codes)) + logger.info( + 'Thanks! Installing: %s. Will take a while...', + _.join(', ', codes), + ) const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes) @@ -89,22 +96,32 @@ function processCryptos (codes) { logger.info('Installation complete.') } -function isEnvironmentValid (crypto) { +function isEnvironmentValid(crypto) { if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_LOCATION`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_LOCATION is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_LOCATION is not set!`, + ) if (_.isEmpty(process.env[`${crypto.cryptoCode}_WALLET_LOCATION`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_WALLET_LOCATION is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_WALLET_LOCATION is not set!`, + ) if (isRemoteWallet(crypto) && !isRemoteNode(crypto)) - throw new Error(`Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!`) + throw new Error( + `Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!`, + ) if (isRemoteNode(crypto) && !isRemoteWallet(crypto)) { if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_HOST`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_HOST is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_HOST is not set!`, + ) if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PORT`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PORT is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_PORT is not set!`, + ) if (_.isEmpty(process.env.BLOCKCHAIN_DIR)) throw new Error(`The environment variable for BLOCKCHAIN_DIR is not set!`) @@ -112,28 +129,39 @@ function isEnvironmentValid (crypto) { if (isRemoteWallet(crypto)) { if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_HOST`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_HOST is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_RPC_HOST is not set!`, + ) if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_PORT`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_PORT is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_RPC_PORT is not set!`, + ) if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_USER`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_USER is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_USER is not set!`, + ) if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PASSWORD`])) - throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PASSWORD is not set!`) + throw new Error( + `The environment variable for ${crypto.cryptoCode}_NODE_PASSWORD is not set!`, + ) } return true } -function setupCrypto (crypto) { +function setupCrypto(crypto) { logger.info(`Installing ${crypto.display}...`) - if (!isEnvironmentValid(crypto)) throw new Error(`Environment error for ${crypto.display}`) + if (!isEnvironmentValid(crypto)) + throw new Error(`Environment error for ${crypto.display}`) if (isRemoteWallet(crypto)) { - logger.info(`Environment variable ${crypto.cryptoCode}_WALLET_LOCATION is set as 'remote', so there's no need to install a node in the system. Exiting...`) + logger.info( + `Environment variable ${crypto.cryptoCode}_WALLET_LOCATION is set as 'remote', so there's no need to install a node in the system. Exiting...`, + ) return } @@ -141,7 +169,9 @@ function setupCrypto (crypto) { makeDir.sync(cryptoDir) const cryptoPlugin = plugin(crypto) const oldDir = process.cwd() - const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp', 'blockchain-install') : '/tmp/blockchain-install' + const tmpDir = isDevMode() + ? path.resolve(BLOCKCHAIN_DIR, 'tmp', 'blockchain-install') + : '/tmp/blockchain-install' makeDir.sync(tmpDir) process.chdir(tmpDir) @@ -157,62 +187,74 @@ function setupCrypto (crypto) { process.chdir(oldDir) } -function updateCrypto (crypto) { +function updateCrypto(crypto) { if (!common.isUpdateDependent(crypto.cryptoCode)) return const cryptoPlugin = plugin(crypto) // TODO: we need to refactor the way we retrieve this status, p.e Monero uses two // services with specific names, so each coin should have its implementation. // Currently, it's not a breaking change because only BTC is update dependent - const status = common.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`).trim() + const status = common + .es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`) + .trim() const isCurrentlyRunning = _.includes(status, ['RUNNING', 'STARTING']) - cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), isCurrentlyRunning) + cryptoPlugin.updateCore( + common.getBinaries(crypto.cryptoCode), + isCurrentlyRunning, + ) } -function plugin (crypto) { +function plugin(crypto) { const plugin = PLUGINS[crypto.cryptoCode] if (!plugin) throw new Error(`No such plugin: ${crypto.cryptoCode}`) return plugin } -function getBlockchainSyncStatus (cryptoList) { - return settingsLoader.loadLatest() - .then(settings => { - if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready') +function getBlockchainSyncStatus(cryptoList) { + return settingsLoader.loadLatest().then(settings => { + if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready') - const blockchainStatuses = _.reduce((acc, value) => { - const processStatus = common.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`).trim() - return acc.then(a => { - if (processStatus === 'RUNNING') { - return wallet.checkBlockchainStatus(settings, value.cryptoCode) - .then(res => Promise.resolve({ ...a, [value.cryptoCode]: res })) - } - return Promise.resolve({ ...a }) - }) - }, - Promise.resolve({}), - cryptoList - ) + const blockchainStatuses = _.reduce( + (acc, value) => { + const processStatus = common + .es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`) + .trim() + return acc.then(a => { + if (processStatus === 'RUNNING') { + return wallet + .checkBlockchainStatus(settings, value.cryptoCode) + .then(res => Promise.resolve({ ...a, [value.cryptoCode]: res })) + } + return Promise.resolve({ ...a }) + }) + }, + Promise.resolve({}), + cryptoList, + ) - return blockchainStatuses - }) + return blockchainStatuses + }) } -function isInstalled (crypto) { +function isInstalled(crypto) { return isDevMode() ? isInstalledSoftware(crypto) : isInstalledSoftware(crypto) && isInstalledVolume(crypto) } -function isDisabled (crypto) { +function isDisabled(crypto) { switch (crypto.cryptoCode) { case 'XMR': - return isInstalled(crypto) && 'Installed' || isInstalled(_.find(it => it.code === 'zcash', cryptos)) && 'Insufficient resources. Contact support.' + return ( + (isInstalled(crypto) && 'Installed') || + (isInstalled(_.find(it => it.code === 'zcash', cryptos)) && + 'Insufficient resources. Contact support.') + ) default: return isInstalled(crypto) && 'Installed' } } -function run () { +function run() { const choices = _.flow([ _.filter(c => !c.hideFromInstall), _.map(c => { @@ -220,47 +262,70 @@ function run () { name: c.display, value: c.code, checked: isInstalled(c), - disabled: isDisabled(c) + disabled: isDisabled(c), } }), ])(cryptos) const questions = [] - const validateAnswers = async (answers) => { - if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false } + const validateAnswers = async answers => { + if (_.size(answers) > 2) + return { + message: `Please insert a maximum of two coins to install.`, + isValid: false, + } if ( _.isEmpty(_.difference(['monero', 'zcash'], answers)) || - (_.includes('monero', answers) && isInstalled(_.find(it => it.code === 'zcash', cryptos))) || - (_.includes('zcash', answers) && isInstalled(_.find(it => it.code === 'monero', cryptos))) + (_.includes('monero', answers) && + isInstalled(_.find(it => it.code === 'zcash', cryptos))) || + (_.includes('zcash', answers) && + isInstalled(_.find(it => it.code === 'monero', cryptos))) ) { - return { message: `Zcash and Monero installations are temporarily mutually exclusive, given the space needed for their blockchains. Contact support for more information.`, isValid: false } + return { + message: `Zcash and Monero installations are temporarily mutually exclusive, given the space needed for their blockchains. Contact support for more information.`, + isValid: false, + } } - return getBlockchainSyncStatus(cryptos) - .then(blockchainStatuses => { - const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses)) - if (_.size(answers) + result.syncing > 2) { - return { message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, isValid: false } + return getBlockchainSyncStatus(cryptos).then(blockchainStatuses => { + const result = _.reduce( + (acc, value) => ({ + ...acc, + [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1, + }), + {}, + _.values(blockchainStatuses), + ) + if (_.size(answers) + result.syncing > 2) { + return { + message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, + isValid: false, } + } - if (result.syncing > 2) { - return { message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`, isValid: false } + if (result.syncing > 2) { + return { + message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`, + isValid: false, } + } - return { message: null, isValid: true } - }) + return { message: null, isValid: true } + }) } questions.push({ type: 'checkbox', name: 'crypto', - message: 'Which cryptocurrencies would you like to install?\nTo prevent server resource overloading, only TWO coins should be syncing simultaneously.\nMore coins can be installed after this process is over.', - choices + message: + 'Which cryptocurrencies would you like to install?\nTo prevent server resource overloading, only TWO coins should be syncing simultaneously.\nMore coins can be installed after this process is over.', + choices, }) - inquirer.prompt(questions) + inquirer + .prompt(questions) .then(answers => Promise.all([validateAnswers(answers.crypto), answers])) .then(([res, answers]) => { if (res.isValid) { diff --git a/packages/server/lib/blockchain/litecoin.js b/packages/server/lib/blockchain/litecoin.js index ce128dd0..7522dbfb 100644 --- a/packages/server/lib/blockchain/litecoin.js +++ b/packages/server/lib/blockchain/litecoin.js @@ -8,7 +8,7 @@ module.exports = { setup, updateCore } const coinRec = coinUtils.getCryptoCurrency('LTC') -function setup (dataDir) { +function setup(dataDir) { common.firewall([coinRec.defaultPort]) const config = buildConfig() common.writeFile(path.resolve(dataDir, coinRec.configFile), config) @@ -16,12 +16,17 @@ function setup (dataDir) { common.writeSupervisorConfig(coinRec, cmd) } -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating Litecoin Core. This may take a minute...') common.es(`sudo supervisorctl stop litecoin`) common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Litecoin Core: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Litecoin Core: Package signature do not match!', + ) return } common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`) @@ -31,25 +36,43 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`) common.es(`rm /tmp/litecoin.tar.gz`) - if (common.es(`grep "changetype=" /mnt/blockchains/litecoin/litecoin.conf || true`)) { + if ( + common.es( + `grep "changetype=" /mnt/blockchains/litecoin/litecoin.conf || true`, + ) + ) { common.logger.info(`changetype already defined, skipping...`) } else { common.logger.info(`Enabling bech32 change addresses in config file..`) - common.es(`echo "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`) + common.es( + `echo "\nchangetype=bech32" >> /mnt/blockchains/litecoin/litecoin.conf`, + ) } - if (common.es(`grep "blockfilterindex=" /mnt/blockchains/litecoin/litecoin.conf || true`)) { + if ( + common.es( + `grep "blockfilterindex=" /mnt/blockchains/litecoin/litecoin.conf || true`, + ) + ) { common.logger.info(`blockfilterindex already defined, skipping...`) } else { common.logger.info(`Disabling blockfilterindex in config file..`) - common.es(`echo "\nblockfilterindex=0" >> /mnt/blockchains/litecoin/litecoin.conf`) + common.es( + `echo "\nblockfilterindex=0" >> /mnt/blockchains/litecoin/litecoin.conf`, + ) } - if (common.es(`grep "peerblockfilters=" /mnt/blockchains/litecoin/litecoin.conf || true`)) { + if ( + common.es( + `grep "peerblockfilters=" /mnt/blockchains/litecoin/litecoin.conf || true`, + ) + ) { common.logger.info(`peerblockfilters already defined, skipping...`) } else { common.logger.info(`Disabling peerblockfilters in config file..`) - common.es(`echo "\npeerblockfilters=0" >> /mnt/blockchains/litecoin/litecoin.conf`) + common.es( + `echo "\npeerblockfilters=0" >> /mnt/blockchains/litecoin/litecoin.conf`, + ) } if (isCurrentlyRunning) { @@ -60,7 +83,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Litecoin Core is updated!') } -function buildConfig () { +function buildConfig() { return `rpcuser=lamassuserver rpcpassword=${common.randomPass()} dbcache=500 diff --git a/packages/server/lib/blockchain/monero.js b/packages/server/lib/blockchain/monero.js index 870f3920..679f150e 100644 --- a/packages/server/lib/blockchain/monero.js +++ b/packages/server/lib/blockchain/monero.js @@ -8,7 +8,7 @@ module.exports = { setup, updateCore } const coinRec = utils.getCryptoCurrency('XMR') -function setup (dataDir) { +function setup(dataDir) { common.firewall([coinRec.defaultPort]) const auth = `lamassuserver:${common.randomPass()}` const config = buildConfig(auth) @@ -18,19 +18,26 @@ function setup (dataDir) { common.writeSupervisorConfig(coinRec, cmd, walletCmd) } -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating Monero. This may take a minute...') common.es(`sudo supervisorctl stop monero monero-wallet`) common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Monero: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Monero: Package signature do not match!', + ) return } common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') common.es(`cp /tmp/${coinRec.dir}/monerod /usr/local/bin/monerod`) - common.es(`cp /tmp/${coinRec.dir}/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc`) + common.es( + `cp /tmp/${coinRec.dir}/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc`, + ) common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`) common.es(`rm /tmp/monero.tar.gz`) @@ -42,7 +49,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Monero is updated!') } -function buildConfig (auth) { +function buildConfig(auth) { return `rpc-login=${auth} stagenet=0 restricted-rpc=1 diff --git a/packages/server/lib/blockchain/zcash.js b/packages/server/lib/blockchain/zcash.js index a6baed51..1434cf06 100644 --- a/packages/server/lib/blockchain/zcash.js +++ b/packages/server/lib/blockchain/zcash.js @@ -9,12 +9,17 @@ module.exports = { setup, updateCore } const es = common.es const logger = common.logger -function updateCore (coinRec, isCurrentlyRunning) { +function updateCore(coinRec, isCurrentlyRunning) { common.logger.info('Updating your Zcash wallet. This may take a minute...') common.es(`sudo supervisorctl stop zcash`) common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`) - if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { - common.logger.info('Failed to update Zcash: Package signature do not match!') + if ( + common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== + coinRec.urlHash + ) { + common.logger.info( + 'Failed to update Zcash: Package signature do not match!', + ) return } common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`) @@ -24,11 +29,17 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`) common.es(`rm /tmp/zcash.tar.gz`) - if (common.es(`grep "walletrequirebackup=" /mnt/blockchains/zcash/zcash.conf || true`)) { + if ( + common.es( + `grep "walletrequirebackup=" /mnt/blockchains/zcash/zcash.conf || true`, + ) + ) { common.logger.info(`walletrequirebackup already defined, skipping...`) } else { common.logger.info(`Setting 'walletrequirebackup=false' in config file...`) - common.es(`echo "\nwalletrequirebackup=false" >> /mnt/blockchains/zcash/zcash.conf`) + common.es( + `echo "\nwalletrequirebackup=false" >> /mnt/blockchains/zcash/zcash.conf`, + ) } if (isCurrentlyRunning) { @@ -39,7 +50,7 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Zcash is updated!') } -function setup (dataDir) { +function setup(dataDir) { es('sudo apt-get update') es('sudo apt-get install libgomp1 -y') const coinRec = coinUtils.getCryptoCurrency('ZEC') @@ -54,7 +65,7 @@ function setup (dataDir) { common.writeSupervisorConfig(coinRec, cmd) } -function buildConfig () { +function buildConfig() { return `mainnet=1 addnode=mainnet.z.cash rpcuser=lamassuserver diff --git a/packages/server/lib/blockexplorers/mempool.space.js b/packages/server/lib/blockexplorers/mempool.space.js index 383c1c72..07a6cf66 100644 --- a/packages/server/lib/blockexplorers/mempool.space.js +++ b/packages/server/lib/blockexplorers/mempool.space.js @@ -1,16 +1,18 @@ -const axios = require("axios"); +const axios = require('axios') const getSatBEstimateFee = () => { - return axios.get('https://mempool.space/api/v1/fees/recommended') + return axios + .get('https://mempool.space/api/v1/fees/recommended') .then(r => r.data.hourFee) } const getSatBEstimateFees = () => { - return axios.get('https://mempool.space/api/v1/fees/recommended') + return axios + .get('https://mempool.space/api/v1/fees/recommended') .then(r => r.data) } module.exports = { getSatBEstimateFees, - getSatBEstimateFee -} \ No newline at end of file + getSatBEstimateFee, +} diff --git a/packages/server/lib/cash-in/cash-in-atomic.js b/packages/server/lib/cash-in/cash-in-atomic.js index 4995520e..bd452912 100644 --- a/packages/server/lib/cash-in/cash-in-atomic.js +++ b/packages/server/lib/cash-in/cash-in-atomic.js @@ -8,63 +8,73 @@ const cashInLow = require('./cash-in-low') module.exports = { atomic } -function atomic (machineTx, pi) { +function atomic(machineTx) { const TransactionMode = pgp.txMode.TransactionMode const isolationLevel = pgp.txMode.isolationLevel const mode = new TransactionMode({ tiLevel: isolationLevel.serializable }) - function transaction (t) { + function transaction(t) { const sql = 'select * from cash_in_txs where id=$1' const sql2 = 'select * from bills where cash_in_txs_id=$1' - return t.oneOrNone(sql, [machineTx.id]) - .then(row => { - if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError({ txId: machineTx.id }) + return t.oneOrNone(sql, [machineTx.id]).then(row => { + if (row && row.tx_version >= machineTx.txVersion) + throw new E.StaleTxError({ txId: machineTx.id }) - return t.any(sql2, [machineTx.id]) - .then(billRows => { - const dbTx = cashInLow.toObj(row) + return t.any(sql2, [machineTx.id]).then(billRows => { + const dbTx = cashInLow.toObj(row) - return preProcess(dbTx, machineTx, pi) - .then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx)) - .then(r => { - return insertNewBills(t, billRows, machineTx) - .then(newBills => _.set('newBills', newBills, r)) - }) + return preProcess(dbTx, machineTx) + .then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx)) + .then(r => { + return insertNewBills(t, billRows, machineTx).then(newBills => + _.set('newBills', newBills, r), + ) }) }) + }) } return db.tx({ mode }, transaction) } -function insertNewBills (t, billRows, machineTx) { +function insertNewBills(t, billRows, machineTx) { const bills = pullNewBills(billRows, machineTx) if (_.isEmpty(bills)) return Promise.resolve([]) const dbBills = _.map(cashInLow.massage, bills) - const billsByDestination = _.countBy(_.get(['destination_unit']) ,dbBills) + const billsByDestination = _.countBy(_.get(['destination_unit']), dbBills) - const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time', 'destination_unit'] + const columns = [ + 'id', + 'fiat', + 'fiat_code', + 'crypto_code', + 'cash_in_fee', + 'cash_in_txs_id', + 'device_time', + 'destination_unit', + ] const sql = pgp.helpers.insert(dbBills, columns, 'bills') const deviceID = machineTx.deviceId const sql2 = `update devices set recycler1 = recycler1 + $2, recycler2 = recycler2 + $3, recycler3 = recycler3 + $4, recycler4 = recycler4 + $5, recycler5 = recycler5 + $6, recycler6 = recycler6 + $7 where device_id = $1` - return t.none(sql2, [ - deviceID, - _.defaultTo(0, billsByDestination.recycler1), - _.defaultTo(0, billsByDestination.recycler2), - _.defaultTo(0, billsByDestination.recycler3), - _.defaultTo(0, billsByDestination.recycler4), - _.defaultTo(0, billsByDestination.recycler5), - _.defaultTo(0, billsByDestination.recycler6) - ]) + return t + .none(sql2, [ + deviceID, + _.defaultTo(0, billsByDestination.recycler1), + _.defaultTo(0, billsByDestination.recycler2), + _.defaultTo(0, billsByDestination.recycler3), + _.defaultTo(0, billsByDestination.recycler4), + _.defaultTo(0, billsByDestination.recycler5), + _.defaultTo(0, billsByDestination.recycler6), + ]) .then(() => { return t.none(sql) }) .then(() => bills) } -function pullNewBills (billRows, machineTx) { +function pullNewBills(billRows, machineTx) { if (_.isEmpty(machineTx.bills)) return [] const toBill = _.mapKeys(_.camelCase) @@ -73,7 +83,7 @@ function pullNewBills (billRows, machineTx) { return _.differenceBy(_.get('id'), machineTx.bills, bills) } -function preProcess (dbTx, machineTx, pi) { +function preProcess(dbTx, machineTx) { // Note: The way this works is if we're clear to send, // we mark the transaction as sendPending. // diff --git a/packages/server/lib/cash-in/cash-in-low.js b/packages/server/lib/cash-in/cash-in-low.js index d6331f0b..54428065 100644 --- a/packages/server/lib/cash-in/cash-in-low.js +++ b/packages/server/lib/cash-in/cash-in-low.js @@ -8,26 +8,40 @@ const E = require('../error') const PENDING_INTERVAL_MS = 60 * T.minutes -const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto'] +const massageFields = [ + 'direction', + 'cryptoNetwork', + 'bills', + 'blacklisted', + 'blacklistMessage', + 'addressReuse', + 'promoCodeApplied', + 'validWalletScore', + 'cashInFeeCrypto', +] const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms') -const massage = _.flow(_.omit(massageFields), - convertBigNumFields, _.mapKeys(_.snakeCase)) +const massage = _.flow( + _.omit(massageFields), + convertBigNumFields, + _.mapKeys(_.snakeCase), +) -const massageUpdates = _.flow(_.omit(massageUpdateFields), - convertBigNumFields, _.mapKeys(_.snakeCase)) +const massageUpdates = _.flow( + _.omit(massageUpdateFields), + convertBigNumFields, + _.mapKeys(_.snakeCase), +) -module.exports = {toObj, upsert, insert, update, massage, isClearToSend} +module.exports = { toObj, upsert, insert, update, massage, isClearToSend } -function convertBigNumFields (obj) { +function convertBigNumFields(obj) { const convert = value => - value && BN.isBigNumber(value) - ? value.toString() - : value + value && BN.isBigNumber(value) ? value.toString() : value return _.mapValues(convert, obj) } -function toObj (row) { +function toObj(row) { if (!row) return null const keys = _.keys(row) @@ -35,7 +49,15 @@ function toObj (row) { keys.forEach(key => { const objKey = _.camelCase(key) - if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'commission_percentage', 'raw_ticker_price'])) { + if ( + _.includes(key, [ + 'crypto_atoms', + 'fiat', + 'cash_in_fee', + 'commission_percentage', + 'raw_ticker_price', + ]) + ) { newObj[objKey] = new BN(row[key]) return } @@ -48,35 +70,35 @@ function toObj (row) { return newObj } -function upsert (t, dbTx, preProcessedTx) { +function upsert(t, dbTx, preProcessedTx) { if (!dbTx) { - return insert(t, preProcessedTx) - .then(tx => ({dbTx, tx})) + return insert(t, preProcessedTx).then(tx => ({ dbTx, tx })) } - return update(t, dbTx, diff(dbTx, preProcessedTx)) - .then(tx => ({dbTx, tx})) + return update(t, dbTx, diff(dbTx, preProcessedTx)).then(tx => ({ dbTx, tx })) } -function insert (t, tx) { +function insert(t, tx) { const dbTx = massage(tx) const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *' - return t.one(sql) - .then(toObj) + return t.one(sql).then(toObj) } -function update (t, tx, changes) { +function update(t, tx, changes) { if (_.isEmpty(changes)) return Promise.resolve(tx) - const dbChanges = isFinalTxStage(changes) ? massage(changes) : massageUpdates(changes) - const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') + - pgp.as.format(' where id=$1', [tx.id]) + ' returning *' + const dbChanges = isFinalTxStage(changes) + ? massage(changes) + : massageUpdates(changes) + const sql = + pgp.helpers.update(dbChanges, null, 'cash_in_txs') + + pgp.as.format(' where id=$1', [tx.id]) + + ' returning *' - return t.one(sql) - .then(toObj) + return t.one(sql).then(toObj) } -function diff (oldTx, newTx) { +function diff(oldTx, newTx) { let updatedTx = {} if (!oldTx) throw new Error('oldTx must not be null') @@ -89,10 +111,15 @@ function diff (oldTx, newTx) { if (_.isEqualWith(nilEqual, oldField, newField)) return if (!ensureRatchet(oldField, newField, fieldKey)) { - logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey) + logger.warn( + 'Value from lamassu-machine would violate ratchet [%s]', + fieldKey, + ) logger.warn('Old tx: %j', oldTx) logger.warn('New tx: %j', newTx) - throw new E.RatchetError('Value from lamassu-machine would violate ratchet') + throw new E.RatchetError( + 'Value from lamassu-machine would violate ratchet', + ) } updatedTx[fieldKey] = newField @@ -101,12 +128,29 @@ function diff (oldTx, newTx) { return updatedTx } -function ensureRatchet (oldField, newField, fieldKey) { - const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion', 'batched', 'discount'] - const free = ['sendPending', 'error', 'errorCode', 'customerId', 'discountSource'] +function ensureRatchet(oldField, newField, fieldKey) { + const monotonic = [ + 'cryptoAtoms', + 'fiat', + 'send', + 'sendConfirmed', + 'operatorCompleted', + 'timedout', + 'txVersion', + 'batched', + 'discount', + ] + const free = [ + 'sendPending', + 'error', + 'errorCode', + 'customerId', + 'discountSource', + ] if (_.isNil(oldField)) return true - if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey) + if (_.includes(fieldKey, monotonic)) + return isMonotonic(oldField, newField, fieldKey) if (_.includes(fieldKey, free)) { if (_.isNil(newField)) return false @@ -114,13 +158,14 @@ function ensureRatchet (oldField, newField, fieldKey) { } if (_.isNil(newField)) return false - if (BN.isBigNumber(oldField) && BN.isBigNumber(newField)) return new BN(oldField).eq(newField) + if (BN.isBigNumber(oldField) && BN.isBigNumber(newField)) + return new BN(oldField).eq(newField) if (oldField.toString() === newField.toString()) return true return false } -function isMonotonic (oldField, newField, fieldKey) { +function isMonotonic(oldField, newField, fieldKey) { if (_.isNil(newField)) return false if (_.isBoolean(oldField)) return oldField === newField || !oldField if (BN.isBigNumber(oldField)) return oldField.lte(newField) @@ -129,20 +174,22 @@ function isMonotonic (oldField, newField, fieldKey) { throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`) } -function nilEqual (a, b) { +function nilEqual(a, b) { if (_.isNil(a) && _.isNil(b)) return true return undefined } -function isClearToSend (oldTx, newTx) { +function isClearToSend(oldTx, newTx) { const now = Date.now() - return (newTx.send || newTx.batched) && + return ( + (newTx.send || newTx.batched) && (!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) && - (newTx.created > now - PENDING_INTERVAL_MS) + newTx.created > now - PENDING_INTERVAL_MS + ) } -function isFinalTxStage (txChanges) { +function isFinalTxStage(txChanges) { return txChanges.send || txChanges.batched } diff --git a/packages/server/lib/cash-in/cash-in-tx.js b/packages/server/lib/cash-in/cash-in-tx.js index a4e8c228..8ca54625 100644 --- a/packages/server/lib/cash-in/cash-in-tx.js +++ b/packages/server/lib/cash-in/cash-in-tx.js @@ -25,93 +25,114 @@ case else 'Pending' end` -module.exports = { post, monitorPending, cancel, PENDING_INTERVAL, TRANSACTION_STATES } +module.exports = { + post, + monitorPending, + cancel, + PENDING_INTERVAL, + TRANSACTION_STATES, +} -function post (machineTx, pi) { +function post(machineTx, pi) { logger.silly('Updating cashin tx:', machineTx) - return cashInAtomic.atomic(machineTx, pi) - .then(r => { - const updatedTx = r.tx - let addressReuse = false + return cashInAtomic.atomic(machineTx).then(r => { + const updatedTx = r.tx + let addressReuse = false - const promises = [settingsLoader.loadLatestConfig()] + const promises = [settingsLoader.loadLatestConfig()] - const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero() - if (isFirstPost) { - promises.push( - checkForBlacklisted(updatedTx), - doesTxReuseAddress(updatedTx), - getWalletScore(updatedTx, pi) - ) - } + const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero() + if (isFirstPost) { + promises.push( + checkForBlacklisted(updatedTx), + doesTxReuseAddress(updatedTx), + getWalletScore(updatedTx, pi), + ) + } - return Promise.all(promises) - .then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => { - const { rejectAddressReuse } = configManager.getCompliance(config) - const isBlacklisted = !!blacklisted + return Promise.all(promises).then( + ([ + config, + blacklisted = false, + isReusedAddress = false, + walletScore = null, + ]) => { + const { rejectAddressReuse } = configManager.getCompliance(config) + const isBlacklisted = !!blacklisted - if (isBlacklisted) { - notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false) - } else if (isReusedAddress && rejectAddressReuse) { - notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true) - addressReuse = true - } - return postProcess(r, pi, isBlacklisted, addressReuse, walletScore) - .then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes)) - .then(changes => cashInLow.update(db, updatedTx, changes)) - .then(_.flow( + if (isBlacklisted) { + notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false) + } else if (isReusedAddress && rejectAddressReuse) { + notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true) + addressReuse = true + } + return postProcess(r, pi, isBlacklisted, addressReuse, walletScore) + .then(changes => + _.set( + 'walletScore', + _.isNil(walletScore) ? null : walletScore.score, + changes, + ), + ) + .then(changes => cashInLow.update(db, updatedTx, changes)) + .then( + _.flow( _.set('bills', machineTx.bills), _.set('blacklisted', isBlacklisted), _.set('blacklistMessage', blacklisted?.content), _.set('addressReuse', addressReuse), - _.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid), - )) - }) - }) + _.set( + 'validWalletScore', + _.isNil(walletScore) || walletScore.isValid, + ), + ), + ) + }, + ) + }) } -function registerTrades (pi, r) { +function registerTrades(pi, r) { _.forEach(bill => pi.buy(bill, r.tx), r.newBills) } -function logAction (rec, tx) { +function logAction(rec, tx) { const action = { tx_id: tx.id, action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'), error: rec.error, error_code: rec.errorCode, - tx_hash: rec.txHash + tx_hash: rec.txHash, } const sql = pgp.helpers.insert(action, null, 'cash_in_actions') - return db.none(sql) - .then(_.constant(rec)) + return db.none(sql).then(_.constant(rec)) } -function logActionById (action, _rec, txId) { +function logActionById(action, _rec, txId) { const rec = _.assign(_rec, { action, tx_id: txId }) const sql = pgp.helpers.insert(rec, null, 'cash_in_actions') return db.none(sql) } -function checkForBlacklisted (tx) { +function checkForBlacklisted(tx) { return blacklist.blocked(tx.toAddress) } -function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { +function postProcess(r, pi, isBlacklisted, addressReuse, walletScore) { if (addressReuse) { return Promise.resolve({ operatorCompleted: true, - error: 'Address Reused' + error: 'Address Reused', }) } if (isBlacklisted) { return Promise.resolve({ operatorCompleted: true, - error: 'Blacklisted Address' + error: 'Blacklisted Address', }) } @@ -120,7 +141,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { walletScore: walletScore.score, operatorCompleted: true, error: 'Chain analysis score is above defined threshold', - errorCode: 'scoreThresholdReached' + errorCode: 'scoreThresholdReached', }) } @@ -128,7 +149,8 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({}) - return pi.sendCoins(r.tx) + return pi + .sendCoins(r.tx) .then(txObj => { if (txObj.batched) { return { @@ -136,7 +158,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { batchTime: 'now()^', sendPending: true, error: null, - errorCode: null + errorCode: null, } } @@ -147,7 +169,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { sendTime: 'now()^', sendPending: false, error: null, - errorCode: null + errorCode: null, } }) .catch(err => { @@ -161,17 +183,18 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { sendTime: 'now()^', error: err.message, errorCode: err.name, - sendPending: true + sendPending: true, } }) .then(sendRec => { - pi.notifyOperator(r.tx, sendRec) - .catch((err) => logger.error('Failure sending transaction notification', err)) + pi.notifyOperator(r.tx, sendRec).catch(err => + logger.error('Failure sending transaction notification', err), + ) return logAction(sendRec, r.tx) }) } -function doesTxReuseAddress (tx) { +function doesTxReuseAddress(tx) { const sql = ` SELECT EXISTS ( SELECT DISTINCT to_address FROM ( @@ -181,15 +204,14 @@ function doesTxReuseAddress (tx) { return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists) } -function getWalletScore (tx, pi) { - return pi.isWalletScoringEnabled(tx) - .then(isEnabled => { - if (!isEnabled) return null - return pi.rateAddress(tx.cryptoCode, tx.toAddress) - }) +function getWalletScore(tx, pi) { + return pi.isWalletScoringEnabled(tx).then(isEnabled => { + if (!isEnabled) return null + return pi.rateAddress(tx.cryptoCode, tx.toAddress) + }) } -function monitorPending (settings) { +function monitorPending(settings) { const sql = `select * from cash_in_txs where created > now() - interval $1 and send @@ -203,27 +225,29 @@ function monitorPending (settings) { const tx = cashInLow.toObj(row) const pi = plugins(settings, tx.deviceId) - return post(tx, pi) - .catch(logger.error) + return post(tx, pi).catch(logger.error) } - return db.any(sql, [PENDING_INTERVAL, MAX_PENDING]) + return db + .any(sql, [PENDING_INTERVAL, MAX_PENDING]) .then(rows => pEachSeries(rows, row => processPending(row))) .catch(logger.error) } -function cancel (txId) { +function cancel(txId) { const updateRec = { error: 'Operator cancel', error_code: 'operatorCancel', operator_completed: true, - batch_id: null + batch_id: null, } return Promise.resolve() .then(() => { - return pgp.helpers.update(updateRec, null, 'cash_in_txs') + - pgp.as.format(' where id=$1', [txId]) + return ( + pgp.helpers.update(updateRec, null, 'cash_in_txs') + + pgp.as.format(' where id=$1', [txId]) + ) }) .then(sql => db.result(sql, false)) .then(res => { diff --git a/packages/server/lib/cash-out/cash-out-actions.js b/packages/server/lib/cash-out/cash-out-actions.js index 9c43e21a..07f7b8bd 100644 --- a/packages/server/lib/cash-out/cash-out-actions.js +++ b/packages/server/lib/cash-out/cash-out-actions.js @@ -1,51 +1,63 @@ const _ = require('lodash/fp') const pgp = require('pg-promise')() -module.exports = {logDispense, logActionById, logAction, logError} +module.exports = { logDispense, logActionById, logAction, logError } -function logDispense (t, tx) { - const baseRec = {error: tx.error, error_code: tx.errorCode} +function logDispense(t, tx) { + const baseRec = { error: tx.error, error_code: tx.errorCode } const rec = _.merge(mapDispense(tx), baseRec) const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError' return logAction(t, action, rec, tx) } -function logActionById (t, action, _rec, txId) { - const rec = _.assign(_rec, {action, tx_id: txId, redeem: false}) +function logActionById(t, action, _rec, txId) { + const rec = _.assign(_rec, { action, tx_id: txId, redeem: false }) const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') return t.none(sql) } -function logAction (t, action, _rec, tx) { - const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem, device_id: tx.deviceId}) +function logAction(t, action, _rec, tx) { + const rec = _.assign(_rec, { + action, + tx_id: tx.id, + redeem: !!tx.redeem, + device_id: tx.deviceId, + }) const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') - return t.none(sql) - .then(_.constant(tx)) + return t.none(sql).then(_.constant(tx)) } -function logError (t, action, err, tx) { - return logAction(t, action, { - error: err.message, - error_code: err.name - }, tx) +function logError(t, action, err, tx) { + return logAction( + t, + action, + { + error: err.message, + error_code: err.name, + }, + tx, + ) } -function mapDispense (tx) { +function mapDispense(tx) { const bills = tx.bills if (_.isEmpty(bills)) return {} const res = {} - _.forEach(it => { - const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, '')) - res[`provisioned_${suffix}`] = bills[it].provisioned - res[`denomination_${suffix}`] = bills[it].denomination - res[`dispensed_${suffix}`] = bills[it].dispensed - res[`rejected_${suffix}`] = bills[it].rejected - }, _.times(_.identity(), _.size(bills))) + _.forEach( + it => { + const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, '')) + res[`provisioned_${suffix}`] = bills[it].provisioned + res[`denomination_${suffix}`] = bills[it].denomination + res[`dispensed_${suffix}`] = bills[it].dispensed + res[`rejected_${suffix}`] = bills[it].rejected + }, + _.times(_.identity(), _.size(bills)), + ) return res } diff --git a/packages/server/lib/cash-out/cash-out-atomic.js b/packages/server/lib/cash-out/cash-out-atomic.js index ca5d2464..87d56a18 100644 --- a/packages/server/lib/cash-out/cash-out-atomic.js +++ b/packages/server/lib/cash-out/cash-out-atomic.js @@ -13,170 +13,208 @@ const toObj = helper.toObj module.exports = { atomic } -function atomic (tx, pi, fromClient) { +function atomic(tx, pi, fromClient) { const TransactionMode = pgp.txMode.TransactionMode const isolationLevel = pgp.txMode.isolationLevel const mode = new TransactionMode({ tiLevel: isolationLevel.serializable }) - function transaction (t) { + function transaction(t) { const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE' - return t.oneOrNone(sql, [tx.id]) + return t + .oneOrNone(sql, [tx.id]) .then(toObj) .then(oldTx => { - const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion) + const isStale = fromClient && oldTx && oldTx.txVersion >= tx.txVersion if (isStale) throw new E.StaleTxError({ txId: tx.id }) // Server doesn't bump version, so we just prevent from version being older. - const isStaleFromServer = !fromClient && oldTx && (oldTx.txVersion > tx.txVersion) - if (isStaleFromServer) throw new Error('Stale Error: server triggered', tx.id) + const isStaleFromServer = + !fromClient && oldTx && oldTx.txVersion > tx.txVersion + if (isStaleFromServer) + throw new Error('Stale Error: server triggered', tx.id) - return preProcess(t, oldTx, tx, pi) - .then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx)) + return preProcess(t, oldTx, tx, pi).then(preProcessedTx => + cashOutLow.upsert(t, oldTx, preProcessedTx), + ) }) } return db.tx({ mode }, transaction) } -function preProcess (t, oldTx, newTx, pi) { +function preProcess(t, oldTx, newTx, pi) { if (!oldTx) { - return pi.isHd(newTx) + return pi + .isHd(newTx) .then(isHd => nextHd(t, isHd, newTx)) .then(newTxHd => { - return pi.newAddress(newTxHd) - .then(_.merge(newTxHd)) + return pi.newAddress(newTxHd).then(_.merge(newTxHd)) }) .then(addressedTx => { const rec = { to_address: addressedTx.toAddress, - layer_2_address: addressedTx.layer2Address + layer_2_address: addressedTx.layer2Address, } return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx) }) .catch(err => { - pi.notifyOperator(newTx, { isRedemption: false, error: 'Error while provisioning address' }) - .catch((err) => logger.error('Failure sending transaction notification', err)) - return cashOutActions.logError(t, 'provisionAddress', err, newTx) - .then(() => { throw err }) + pi.notifyOperator(newTx, { + isRedemption: false, + error: 'Error while provisioning address', + }).catch(err => + logger.error('Failure sending transaction notification', err), + ) + return cashOutActions + .logError(t, 'provisionAddress', err, newTx) + .then(() => { + throw err + }) }) } - return Promise.resolve(updateStatus(oldTx, newTx)) - .then(updatedTx => { - if (updatedTx.status !== oldTx.status) { - const isZeroConf = pi.isZeroConf(updatedTx) - updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf) + return Promise.resolve(updateStatus(oldTx, newTx)).then(updatedTx => { + if (updatedTx.status !== oldTx.status) { + const isZeroConf = pi.isZeroConf(updatedTx) + updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf) - const rec = { - to_address: updatedTx.toAddress, - tx_hash: updatedTx.txHash - } - - return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx) + const rec = { + to_address: updatedTx.toAddress, + tx_hash: updatedTx.txHash, } - const hasError = !oldTx.error && newTx.error - const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills) + return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx) + } - if (hasError || hasDispenseOccurred) { - return cashOutActions.logDispense(t, updatedTx) - .then(it => updateCassettes(t, updatedTx).then(() => it) ) - .then((t) => { - pi.notifyOperator(updatedTx, { isRedemption: true }) - .catch((err) => logger.error('Failure sending transaction notification', err)) - return t - }) - } + const hasError = !oldTx.error && newTx.error + const hasDispenseOccurred = + !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills) - if (!oldTx.phone && newTx.phone) { - return cashOutActions.logAction(t, 'addPhone', {}, updatedTx) - } + if (hasError || hasDispenseOccurred) { + return cashOutActions + .logDispense(t, updatedTx) + .then(it => updateCassettes(t, updatedTx).then(() => it)) + .then(t => { + pi.notifyOperator(updatedTx, { isRedemption: true }).catch(err => + logger.error('Failure sending transaction notification', err), + ) + return t + }) + } - if (!oldTx.redeem && newTx.redeem) { - return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx) - } + if (!oldTx.phone && newTx.phone) { + return cashOutActions.logAction(t, 'addPhone', {}, updatedTx) + } - return updatedTx - }) + if (!oldTx.redeem && newTx.redeem) { + return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx) + } + + return updatedTx + }) } -function nextHd (t, isHd, tx) { +function nextHd(t, isHd, tx) { if (!isHd) return Promise.resolve(tx) - return t.one("select nextval('hd_indices_seq') as hd_index") + return t + .one("select nextval('hd_indices_seq') as hd_index") .then(row => _.set('hdIndex', row.hd_index, tx)) } -function updateCassettes (t, tx) { +function updateCassettes(t, tx) { if (!dispenseOccurred(tx.bills)) return Promise.resolve() - const billsStmt = _.join(', ')(_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(_.range(0, _.size(tx.bills)))) + const billsStmt = _.join(', ')( + _.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)( + _.range(0, _.size(tx.bills)), + ), + ) const returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills)) const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}` const values = [] - _.forEach(it => values.push( - tx.bills[it].dispensed + tx.bills[it].rejected - ), _.times(_.identity(), _.size(tx.bills))) + _.forEach( + it => values.push(tx.bills[it].dispensed + tx.bills[it].rejected), + _.times(_.identity(), _.size(tx.bills)), + ) values.push(tx.deviceId) return t.one(sql, values) } -function wasJustAuthorized (oldTx, newTx, isZeroConf) { - const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published', 'rejected']) && +function wasJustAuthorized(oldTx, newTx, isZeroConf) { + const isAuthorized = () => + _.includes(oldTx.status, ['notSeen', 'published', 'rejected']) && _.includes(newTx.status, ['authorized', 'instant', 'confirmed']) - const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized', 'rejected']) && - _.includes(newTx.status, ['instant', 'confirmed']) + const isConfirmed = () => + _.includes(oldTx.status, [ + 'notSeen', + 'published', + 'authorized', + 'rejected', + ]) && _.includes(newTx.status, ['instant', 'confirmed']) return isZeroConf ? isAuthorized() : isConfirmed() } -function isPublished (status) { - return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed']) +function isPublished(status) { + return _.includes(status, [ + 'published', + 'rejected', + 'authorized', + 'instant', + 'confirmed', + ]) } -function isConfirmed (status) { +function isConfirmed(status) { return status === 'confirmed' } -function updateStatus (oldTx, newTx) { +function updateStatus(oldTx, newTx) { const oldStatus = oldTx.status const newStatus = ratchetStatus(oldStatus, newTx.status) - const publishedAt = !oldTx.publishedAt && isPublished(newStatus) - ? 'now()^' - : undefined + const publishedAt = + !oldTx.publishedAt && isPublished(newStatus) ? 'now()^' : undefined - const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus) - ? 'now()^' - : undefined + const confirmedAt = + !oldTx.confirmedAt && isConfirmed(newStatus) ? 'now()^' : undefined const updateRec = { publishedAt, confirmedAt, - status: newStatus + status: newStatus, } return _.merge(newTx, updateRec) } -function ratchetStatus (oldStatus, newStatus) { - const statusOrder = ['notSeen', 'published', 'rejected', - 'authorized', 'instant', 'confirmed'] +function ratchetStatus(oldStatus, newStatus) { + const statusOrder = [ + 'notSeen', + 'published', + 'rejected', + 'authorized', + 'instant', + 'confirmed', + ] if (oldStatus === newStatus) return oldStatus if (newStatus === 'insufficientFunds') return newStatus - const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus)) + const idx = Math.max( + statusOrder.indexOf(oldStatus), + statusOrder.indexOf(newStatus), + ) return statusOrder[idx] } -function dispenseOccurred (bills) { +function dispenseOccurred(bills) { if (_.isEmpty(bills)) return false return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills) } diff --git a/packages/server/lib/cash-out/cash-out-helper.js b/packages/server/lib/cash-out/cash-out-helper.js index 1b34b155..92919069 100644 --- a/packages/server/lib/cash-out/cash-out-helper.js +++ b/packages/server/lib/cash-out/cash-out-helper.js @@ -40,18 +40,31 @@ const SNAKE_CASE_BILL_FIELDS = [ 'provisioned_recycler_3', 'provisioned_recycler_4', 'provisioned_recycler_5', - 'provisioned_recycler_6' + 'provisioned_recycler_6', ] const BILL_FIELDS = _.map(_.camelCase, SNAKE_CASE_BILL_FIELDS) -module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } +module.exports = { + redeemableTxs, + toObj, + toDb, + REDEEMABLE_AGE, + CASH_OUT_TRANSACTION_STATES, +} -const mapValuesWithKey = _.mapValues.convert({cap: false}) +const mapValuesWithKey = _.mapValues.convert({ cap: false }) -function convertBigNumFields (obj) { +function convertBigNumFields(obj) { const convert = (value, key) => { - if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) { + if ( + _.includes(key, [ + 'cryptoAtoms', + 'receivedCryptoAtoms', + 'fiat', + 'fixedFee', + ]) + ) { // BACKWARDS_COMPATIBILITY 10.1 // bills before 10.2 don't have fixedFee if (key === 'fixedFee' && !value) return new BN(0).toString() @@ -59,62 +72,62 @@ function convertBigNumFields (obj) { } // Only test isNil for these fields since the others should not be empty. - if (_.includes(key, [ 'commissionPercentage', 'rawTickerPrice' ]) && !_.isNil(value)) { + if ( + _.includes(key, ['commissionPercentage', 'rawTickerPrice']) && + !_.isNil(value) + ) { return value.toString() } return value } - const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat']) - ? key + '#' - : key + const convertKey = key => + _.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key return _.mapKeys(convertKey, mapValuesWithKey(convert, obj)) } -function convertField (key) { +function convertField(key) { return _.snakeCase(key) } -function addDbBills (tx) { +function addDbBills(tx) { const bills = tx.bills if (_.isEmpty(bills)) return tx const billsObj = _.flow( - _.reduce( - (acc, value) => { - const suffix = _.snakeCase(value.name.replace(/cassette/gi, '')) - return { - ...acc, - [`provisioned_${suffix}`]: value.provisioned, - [`denomination_${suffix}`]: value.denomination - } - }, - {} - ), + _.reduce((acc, value) => { + const suffix = _.snakeCase(value.name.replace(/cassette/gi, '')) + return { + ...acc, + [`provisioned_${suffix}`]: value.provisioned, + [`denomination_${suffix}`]: value.denomination, + } + }, {}), it => { - const missingKeys = _.reduce( - (acc, value) => { - return _.assign({ [value]: 0 })(acc) - }, - {} - )(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it))) + const missingKeys = _.reduce((acc, value) => { + return _.assign({ [value]: 0 })(acc) + }, {})(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it))) return _.assign(missingKeys, it) - } + }, )(bills) return _.assign(tx, billsObj) } -function toDb (tx) { - const massager = _.flow(convertBigNumFields, addDbBills, - _.omit(['direction', 'bills', 'promoCodeApplied']), _.mapKeys(convertField)) +function toDb(tx) { + const massager = _.flow( + convertBigNumFields, + addDbBills, + _.omit(['direction', 'bills', 'promoCodeApplied']), + _.mapKeys(convertField), + ) return massager(tx) } -function toObj (row) { +function toObj(row) { if (!row) return null const keys = _.keys(row) @@ -126,7 +139,14 @@ function toObj (row) { newObj[objKey] = new BN(row[key]) return } - if (_.includes(key, ['crypto_atoms', 'fiat', 'commission_percentage', 'raw_ticker_price'])) { + if ( + _.includes(key, [ + 'crypto_atoms', + 'fiat', + 'commission_percentage', + 'raw_ticker_price', + ]) + ) { newObj[objKey] = new BN(row[key]) return } @@ -137,11 +157,20 @@ function toObj (row) { newObj.direction = 'cashOut' if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj - if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) throw new Error('Missing cassette values') + if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) + throw new Error('Missing cassette values') const billFieldsArr = _.concat( - _.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)), - _.map(it => ({ name: `recycler${it + 1}`, denomination: newObj[`denominationRecycler${it + 1}`], provisioned: newObj[`provisionedRecycler${it + 1}`] }))(_.range(0, MAX_RECYCLERS)), + _.map(it => ({ + name: `cassette${it + 1}`, + denomination: newObj[`denomination${it + 1}`], + provisioned: newObj[`provisioned${it + 1}`], + }))(_.range(0, MAX_CASSETTES)), + _.map(it => ({ + name: `recycler${it + 1}`, + denomination: newObj[`denominationRecycler${it + 1}`], + provisioned: newObj[`provisionedRecycler${it + 1}`], + }))(_.range(0, MAX_RECYCLERS)), ) // There can't be bills with denomination === 0. @@ -151,7 +180,7 @@ function toObj (row) { return _.set('bills', bills, _.omit(BILL_FIELDS, newObj)) } -function redeemableTxs (deviceId) { +function redeemableTxs(deviceId) { const sql = `select * from cash_out_txs where device_id=$1 and redeem=$2 @@ -164,6 +193,5 @@ function redeemableTxs (deviceId) { ) and extract(epoch from (now() - greatest(created, confirmed_at))) < $4` - return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE]) - .then(_.map(toObj)) + return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE]).then(_.map(toObj)) } diff --git a/packages/server/lib/cash-out/cash-out-low.js b/packages/server/lib/cash-out/cash-out-low.js index 24aea40c..29d4bad8 100644 --- a/packages/server/lib/cash-out/cash-out-low.js +++ b/packages/server/lib/cash-out/cash-out-low.js @@ -7,73 +7,91 @@ const { anonymousCustomer } = require('../constants') const toDb = helper.toDb const toObj = helper.toObj -const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed', - 'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode', - 'receivedCryptoAtoms', 'walletScore', 'customerId' ] +const UPDATEABLE_FIELDS = [ + 'txHash', + 'txVersion', + 'status', + 'dispense', + 'dispenseConfirmed', + 'notified', + 'redeem', + 'phone', + 'error', + 'swept', + 'publishedAt', + 'confirmedAt', + 'errorCode', + 'receivedCryptoAtoms', + 'walletScore', + 'customerId', +] -module.exports = {upsert, update, insert} +module.exports = { upsert, update, insert } -function upsert (t, oldTx, tx) { +function upsert(t, oldTx, tx) { if (!oldTx) { - return insert(t, tx) - .then(newTx => [oldTx, newTx]) + return insert(t, tx).then(newTx => [oldTx, newTx]) } - return update(t, tx, diff(oldTx, tx)) - .then(newTx => [oldTx, newTx, tx.justAuthorized]) + return update(t, tx, diff(oldTx, tx)).then(newTx => [ + oldTx, + newTx, + tx.justAuthorized, + ]) } -function insert (t, tx) { +function insert(t, tx) { const dbTx = toDb(tx) const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *' - return t.one(sql) - .then(toObj) + return t.one(sql).then(toObj) } -function update (t, tx, changes) { +function update(t, tx, changes) { if (_.isEmpty(changes)) return Promise.resolve(tx) const dbChanges = toDb(changes) - const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + + const sql = + pgp.helpers.update(dbChanges, null, 'cash_out_txs') + pgp.as.format(' where id=$1', [tx.id]) const newTx = _.merge(tx, changes) - return t.none(sql) - .then(() => newTx) + return t.none(sql).then(() => newTx) } -function diff (oldTx, newTx) { +function diff(oldTx, newTx) { let updatedTx = {} UPDATEABLE_FIELDS.forEach(fieldKey => { - if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return + if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) + return // We never null out an existing field - if (oldTx && _.isNil(newTx[fieldKey])) return updatedTx[fieldKey] = oldTx[fieldKey] + if (oldTx && _.isNil(newTx[fieldKey])) + return (updatedTx[fieldKey] = oldTx[fieldKey]) switch (fieldKey) { case 'customerId': if (oldTx.customerId === anonymousCustomer.uuid) { - return updatedTx['customerId'] = newTx['customerId'] + return (updatedTx['customerId'] = newTx['customerId']) } return // prevent dispense changing from 'true' to 'false' case 'dispense': if (!oldTx.dispense) { - return updatedTx[fieldKey] = newTx[fieldKey] + return (updatedTx[fieldKey] = newTx[fieldKey]) } return default: - return updatedTx[fieldKey] = newTx[fieldKey] + return (updatedTx[fieldKey] = newTx[fieldKey]) } }) return updatedTx } -function nilEqual (a, b) { +function nilEqual(a, b) { if (_.isNil(a) && _.isNil(b)) return true return undefined diff --git a/packages/server/lib/cash-out/cash-out-tx.js b/packages/server/lib/cash-out/cash-out-tx.js index 93274e4d..2944f450 100644 --- a/packages/server/lib/cash-out/cash-out-tx.js +++ b/packages/server/lib/cash-out/cash-out-tx.js @@ -20,7 +20,7 @@ module.exports = { monitorLiveIncoming, monitorStaleIncoming, monitorUnnotified, - cancel + cancel, } const STALE_INCOMING_TX_AGE = T.day @@ -31,38 +31,40 @@ const INSUFFICIENT_FUNDS_CODE = 570 const toObj = helper.toObj -function selfPost (tx, pi) { +function selfPost(tx, pi) { return post(tx, pi, false) } -function post (tx, pi, fromClient = true) { +function post(tx, pi, fromClient = true) { logger.silly('Updating cashout -- tx:', JSON.stringify(tx)) logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient)) - return cashOutAtomic.atomic(tx, pi, fromClient) - .then(txVector => { - const [, newTx, justAuthorized] = txVector - return postProcess(txVector, justAuthorized, pi) - .then(changes => cashOutLow.update(db, newTx, changes)) - }) + return cashOutAtomic.atomic(tx, pi, fromClient).then(txVector => { + const [, newTx, justAuthorized] = txVector + return postProcess(txVector, justAuthorized, pi).then(changes => + cashOutLow.update(db, newTx, changes), + ) + }) } -function postProcess (txVector, justAuthorized, pi) { +function postProcess(txVector, justAuthorized, pi) { const [oldTx, newTx] = txVector if (justAuthorized) { pi.sell(newTx) - pi.notifyOperator(newTx, { isRedemption: false }) - .catch((err) => logger.error('Failure sending transaction notification', err)) + pi.notifyOperator(newTx, { isRedemption: false }).catch(err => + logger.error('Failure sending transaction notification', err), + ) } if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) { - return pi.buildAvailableUnits(newTx.id) + return pi + .buildAvailableUnits(newTx.id) .then(units => { units = _.concat(units.cassettes, units.recyclers) logger.silly('Computing bills to dispense:', { txId: newTx.id, units: units, - fiat: newTx.fiat + fiat: newTx.fiat, }) const bills = billMath.makeChange(units, newTx.fiat) logger.silly('Bills to dispense:', JSON.stringify(bills)) @@ -73,27 +75,38 @@ function postProcess (txVector, justAuthorized, pi) { .then(bills => { const rec = {} - _.forEach(it => { - const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, '')) - rec[`provisioned_${suffix}`] = bills[it].provisioned - rec[`denomination_${suffix}`] = bills[it].denomination - }, _.times(_.identity(), _.size(bills))) + _.forEach( + it => { + const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, '')) + rec[`provisioned_${suffix}`] = bills[it].provisioned + rec[`denomination_${suffix}`] = bills[it].denomination + }, + _.times(_.identity(), _.size(bills)), + ) - return cashOutActions.logAction(db, 'provisionNotes', rec, newTx) + return cashOutActions + .logAction(db, 'provisionNotes', rec, newTx) .then(_.constant({ bills })) }) .catch(err => { - pi.notifyOperator(newTx, { error: err.message, isRedemption: true }) - .catch((err) => logger.error('Failure sending transaction notification', err)) - return cashOutActions.logError(db, 'provisionNotesError', err, newTx) - .then(() => { throw err }) + pi.notifyOperator(newTx, { + error: err.message, + isRedemption: true, + }).catch(err => + logger.error('Failure sending transaction notification', err), + ) + return cashOutActions + .logError(db, 'provisionNotesError', err, newTx) + .then(() => { + throw err + }) }) } return Promise.resolve({}) } -function fetchOpenTxs (statuses, fromAge, toAge) { +function fetchOpenTxs(statuses, fromAge, toAge) { const sql = `select * from cash_out_txs where ((extract(epoch from (now() - created))) * 1000)>$1 @@ -103,20 +116,27 @@ function fetchOpenTxs (statuses, fromAge, toAge) { const statusClause = _.map(pgp.as.text, statuses).join(',') - return db.any(sql, [fromAge, toAge, statusClause]) + return db + .any(sql, [fromAge, toAge, statusClause]) .then(rows => rows.map(toObj)) } -function processTxStatus (tx, settings) { +function processTxStatus(tx, settings) { const pi = plugins(settings, tx.deviceId) - return pi.getStatus(tx) - .then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status })) + return pi + .getStatus(tx) + .then(res => + _.assign(tx, { + receivedCryptoAtoms: res.receivedCryptoAtoms, + status: res.status, + }), + ) .then(_tx => getWalletScore(_tx, pi)) .then(_tx => selfPost(_tx, pi)) } -function getWalletScore (tx, pi) { +function getWalletScore(tx, pi) { const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds'] if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) { @@ -124,40 +144,54 @@ function getWalletScore (tx, pi) { } // Transaction shows up on the blockchain, we can request the sender address - return pi.isWalletScoringEnabled(tx) - .then(isEnabled => { - if (!isEnabled) return tx - return pi.rateTransaction(tx) - .then(res => - res.isValid - ? _.assign(tx, { walletScore: res.score }) - : _.assign(tx, { + return pi.isWalletScoringEnabled(tx).then(isEnabled => { + if (!isEnabled) return tx + return pi + .rateTransaction(tx) + .then(res => + res.isValid + ? _.assign(tx, { walletScore: res.score }) + : _.assign(tx, { walletScore: res.score, error: 'Chain analysis score is above defined threshold', errorCode: 'scoreThresholdReached', - dispense: true - }) - ) - .catch(error => _.assign(tx, { + dispense: true, + }), + ) + .catch(error => + _.assign(tx, { walletScore: 10, error: `Failure getting address score: ${error.message}`, errorCode: 'walletScoringError', - dispense: true - })) - }) + dispense: true, + }), + ) + }) } -function monitorLiveIncoming (settings) { +function monitorLiveIncoming(settings) { const statuses = ['notSeen', 'published', 'insufficientFunds'] return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE) } -function monitorStaleIncoming (settings) { - const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds'] - return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE) +function monitorStaleIncoming(settings) { + const statuses = [ + 'notSeen', + 'published', + 'authorized', + 'instant', + 'rejected', + 'insufficientFunds', + ] + return monitorIncoming( + settings, + statuses, + STALE_LIVE_INCOMING_TX_AGE, + STALE_INCOMING_TX_AGE, + ) } -function monitorIncoming (settings, statuses, fromAge, toAge) { +function monitorIncoming(settings, statuses, fromAge, toAge) { return fetchOpenTxs(statuses, fromAge, toAge) .then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings))) .catch(err => { @@ -169,7 +203,7 @@ function monitorIncoming (settings, statuses, fromAge, toAge) { }) } -function monitorUnnotified (settings) { +function monitorUnnotified(settings) { const sql = `select * from cash_out_txs where ((extract(epoch from (now() - created))) * 1000)<$1 @@ -179,23 +213,26 @@ function monitorUnnotified (settings) { and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)` const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx) - return db.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE]) + return db + .any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE]) .then(rows => _.map(toObj, rows)) .then(txs => Promise.all(txs.map(notify))) .catch(logger.error) } -function cancel (txId) { +function cancel(txId) { const updateRec = { error: 'Operator cancel', error_code: 'operatorCancel', - dispense: true + dispense: true, } return Promise.resolve() .then(() => { - return pgp.helpers.update(updateRec, null, 'cash_out_txs') + - pgp.as.format(' where id=$1', [txId]) + return ( + pgp.helpers.update(updateRec, null, 'cash_out_txs') + + pgp.as.format(' where id=$1', [txId]) + ) }) .then(sql => db.result(sql, false)) .then(res => { diff --git a/packages/server/lib/cashbox-batches.js b/packages/server/lib/cashbox-batches.js index 43205308..f3ad34d3 100644 --- a/packages/server/lib/cashbox-batches.js +++ b/packages/server/lib/cashbox-batches.js @@ -4,8 +4,9 @@ const _ = require('lodash/fp') const uuid = require('uuid') const camelize = require('./utils') -function createCashboxBatch (deviceId, cashboxCount) { - if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.') +function createCashboxBatch(deviceId, cashboxCount) { + if (_.isEqual(0, cashboxCount)) + throw new Error('Cash box is empty. Cash box batch could not be created.') const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *` const sql2 = ` UPDATE bills SET cashbox_batch_id=$1 @@ -24,55 +25,93 @@ function createCashboxBatch (deviceId, cashboxCount) { const q1 = t.one(sql, [batchId, deviceId]) const q2 = t.none(sql2, [batchId, deviceId]) const q3 = t.none(sql3, [batchId, deviceId]) - return t.batch([q1, q2, q3]) - .then(([it]) => it) + return t.batch([q1, q2, q3]).then(([it]) => it) }) } -function updateMachineWithBatch (machineContext, oldCashboxCount) { +function updateMachineWithBatch(machineContext, oldCashboxCount) { const cashUnits = machineContext.cashUnits - const cashUnitNames = ['cashbox', 'cassette1', 'cassette2', 'cassette3', 'cassette4', 'recycler1', 'recycler2', 'recycler3', 'recycler4', 'recycler5', 'recycler6'] - const isValidContext = _.has(['deviceId', 'cashUnits'], machineContext) && _.has(cashUnitNames, cashUnits) - const cassettes = _.filter(it => !_.isNil(it))([cashUnits.cassette1, cashUnits.cassette2, cashUnits.cassette3, cashUnits.cassette4]) - const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(cassettes)) + const cashUnitNames = [ + 'cashbox', + 'cassette1', + 'cassette2', + 'cassette3', + 'cassette4', + 'recycler1', + 'recycler2', + 'recycler3', + 'recycler4', + 'recycler5', + 'recycler6', + ] + const isValidContext = + _.has(['deviceId', 'cashUnits'], machineContext) && + _.has(cashUnitNames, cashUnits) + const cassettes = _.filter(it => !_.isNil(it))([ + cashUnits.cassette1, + cashUnits.cassette2, + cashUnits.cassette3, + cashUnits.cassette4, + ]) + const isCassetteAmountWithinRange = _.inRange( + constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, + constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, + _.size(cassettes), + ) if (!isValidContext && !isCassetteAmountWithinRange) throw new Error('Insufficient info to create a new cashbox batch') - if (_.isEqual(0, oldCashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.') + if (_.isEqual(0, oldCashboxCount)) + throw new Error('Cash box is empty. Cash box batch could not be created.') return db.tx(t => { const deviceId = machineContext.deviceId const batchId = uuid.v4() - const q1 = t.none(`INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, [batchId, deviceId]) - const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs + const q1 = t.none( + `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, + [batchId, deviceId], + ) + const q2 = t.none( + `UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs WHERE bills.cash_in_txs_id = cash_in_txs.id AND cash_in_txs.device_id = $2 AND bills.destination_unit = 'cashbox' AND - bills.cashbox_batch_id IS NULL`, [batchId, deviceId]) - const q3 = t.none(`UPDATE empty_unit_bills SET cashbox_batch_id=$1 - WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, [batchId, deviceId]) - const q4 = t.none(` + bills.cashbox_batch_id IS NULL`, + [batchId, deviceId], + ) + const q3 = t.none( + `UPDATE empty_unit_bills SET cashbox_batch_id=$1 + WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, + [batchId, deviceId], + ) + const q4 = t.none( + ` UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3), recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6) WHERE device_id=$11 - `, [ - cashUnits.cassette1, - cashUnits.cassette2, - cashUnits.cassette3, - cashUnits.cassette4, - cashUnits.recycler1, - cashUnits.recycler2, - cashUnits.recycler3, - cashUnits.recycler4, - cashUnits.recycler5, - cashUnits.recycler6, - machineContext.deviceId - ]) + `, + [ + cashUnits.cassette1, + cashUnits.cassette2, + cashUnits.cassette3, + cashUnits.cassette4, + cashUnits.recycler1, + cashUnits.recycler2, + cashUnits.recycler3, + cashUnits.recycler4, + cashUnits.recycler5, + cashUnits.recycler6, + machineContext.deviceId, + ], + ) return t.batch([q1, q2, q3, q4]) }) } -function getBatches (from = new Date(0).toISOString(), until = new Date().toISOString()) { +function getBatches( + from = new Date(0).toISOString(), + until = new Date().toISOString(), +) { const sql = ` SELECT cuo.id, @@ -96,28 +135,25 @@ function getBatches (from = new Date(0).toISOString(), until = new Date().toISOS return db.any(sql, [from, until]).then(camelize) } -function editBatchById (id, performedBy) { +function editBatchById(id, performedBy) { const sql = `UPDATE cash_unit_operation SET performed_by=$1 WHERE id=$2 AND cuo.operation_type = 'cash-box-empty'` return db.none(sql, [performedBy, id]) } -function logFormatter (data) { - return _.map( - it => { - return { - id: it.id, - deviceId: it.deviceId, - created: it.created, - operationType: it.operationType, - billCount: it.billCount, - fiatTotal: it.fiatTotal - } - }, - data - ) +function logFormatter(data) { + return _.map(it => { + return { + id: it.id, + deviceId: it.deviceId, + created: it.created, + operationType: it.operationType, + billCount: it.billCount, + fiatTotal: it.fiatTotal, + } + }, data) } -function getMachineUnbatchedBills (deviceId) { +function getMachineUnbatchedBills(deviceId) { const sql = ` SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs ON b.cash_in_txs_id = cash_in_txs.id @@ -125,12 +161,13 @@ function getMachineUnbatchedBills (deviceId) { GROUP BY cash_in_txs.device_id ` - return db.oneOrNone(sql, [deviceId]) + return db + .oneOrNone(sql, [deviceId]) .then(res => _.mapKeys(it => _.camelCase(it), res)) .then(logFormatterSingle) } -function getBatchById (id) { +function getBatchById(id) { const sql = ` SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills FROM cash_unit_operation AS cb @@ -139,14 +176,22 @@ function getBatchById (id) { GROUP BY cb.id ` - return db.oneOrNone(sql, [id]).then(res => _.mapKeys(it => _.camelCase(it), res)) + return db + .oneOrNone(sql, [id]) + .then(res => _.mapKeys(it => _.camelCase(it), res)) .then(logFormatterSingle) } -function logFormatterSingle (data) { +function logFormatterSingle(data) { const bills = _.filter( - it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)), - data.bills + it => + !( + _.isNil(it) || + _.isNil(it.fiat_code) || + _.isNil(it.fiat) || + _.isNaN(it.fiat) + ), + data.bills, ) return { @@ -161,9 +206,9 @@ function logFormatterSingle (data) { return acc }, {}, - bills + bills, ), - billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills) + billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills), } } @@ -174,5 +219,5 @@ module.exports = { editBatchById, getBatchById, getMachineUnbatchedBills, - logFormatter + logFormatter, } diff --git a/packages/server/lib/coin-change.js b/packages/server/lib/coin-change.js index 4a1c964a..38e42e92 100644 --- a/packages/server/lib/coin-change.js +++ b/packages/server/lib/coin-change.js @@ -9,16 +9,13 @@ */ const prepare_denominations = denominations => JSON.parse(JSON.stringify(denominations)) - .sort(([d1, c1], [d2, c2]) => d1 < d2) + .sort(([d1], [d2]) => d1 < d2) .reduce( ([csum, denoms], [denom, count]) => { - csum += denom*count - return [ - csum, - [{ denom, count, csum }].concat(denoms) - ] + csum += denom * count + return [csum, [{ denom, count, csum }].concat(denoms)] }, - [0, []] + [0, []], )[1] /* ([csum, denoms]) => denoms */ const max_denomination_multiplicity = (denom, count, target) => @@ -38,18 +35,18 @@ const memo_get = (memo, target, denom) => { const memo_set = (memo, target, denom, solution) => { let denom_solutions = memo[target] - if (denom_solutions === undefined) - memo[target] = denom_solutions = {} - return denom_solutions[denom] = solution + if (denom_solutions === undefined) memo[target] = denom_solutions = {} + return (denom_solutions[denom] = solution) } const check = (solution, target) => - !solution - || target === solution.reduce((sum, [denom, provisioned]) => sum + denom*provisioned, 0) + !solution || + target === + solution.reduce((sum, [denom, provisioned]) => sum + denom * provisioned, 0) const model = denominations => ({ denominations: prepare_denominations(denominations), - memo: {} + memo: {}, }) /* @@ -73,16 +70,19 @@ const solve = (model, target) => { * of the denominations, or if the target is not divisible by any of the * denominations */ - if (target > csum) - return memo_set(memo, target, denom, false) + if (target > csum) return memo_set(memo, target, denom, false) let solution = memo_get(memo, target, denom) if (solution === false) continue /* not here, keep looking */ if (solution) return solution /* we've previously computed a solution */ /* solution === null */ - for (let nd = max_denomination_multiplicity(denom, count, target); nd >= 0; nd--) { - solution = coin_change(didx+1, target - denom*nd) + for ( + let nd = max_denomination_multiplicity(denom, count, target); + nd >= 0; + nd-- + ) { + solution = coin_change(didx + 1, target - denom * nd) if (solution) return memo_set(memo, target, denom, [[denom, nd]].concat(solution)) } diff --git a/packages/server/lib/coinatmradar/coinatmradar.js b/packages/server/lib/coinatmradar/coinatmradar.js index 88b9eaf4..0c9eddab 100644 --- a/packages/server/lib/coinatmradar/coinatmradar.js +++ b/packages/server/lib/coinatmradar/coinatmradar.js @@ -18,9 +18,11 @@ const STALE_INTERVAL = '2 minutes' module.exports = { update } -function mapCoin (rates, deviceId, settings, cryptoCode) { +function mapCoin(rates, deviceId, settings, cryptoCode) { const config = settings.config - const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null } + const buildedRates = plugins(settings, deviceId).buildRates(rates)[ + cryptoCode + ] || { cashIn: null, cashOut: null } const commissions = configManager.getCommissions(cryptoCode, deviceId, config) const coinAtmRadar = configManager.getCoinAtmRadar(config) @@ -30,8 +32,12 @@ function mapCoin (rates, deviceId, settings, cryptoCode) { const cashOutFee = showCommissions ? commissions.cashOut / 100 : null const cashInFixedFee = showCommissions ? commissions.fixedFee : null const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null - const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null - const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null + const cashInRate = showCommissions + ? _.invoke('cashIn.toNumber', buildedRates) + : null + const cashOutRate = showCommissions + ? _.invoke('cashOut.toNumber', buildedRates) + : null return { cryptoCode, @@ -40,11 +46,11 @@ function mapCoin (rates, deviceId, settings, cryptoCode) { cashInFixedFee, cashOutFixedFee, cashInRate, - cashOutRate + cashOutRate, } } -function mapIdentification (config) { +function mapIdentification(config) { const triggers = configManager.getTriggers(config) return { @@ -52,11 +58,11 @@ function mapIdentification (config) { isPalmVein: false, isPhoto: complianceTriggers.hasFacephoto(triggers), isIdDocScan: complianceTriggers.hasIdScan(triggers), - isFingerprint: false + isFingerprint: false, } } -function mapMachine (rates, settings, machineRow) { +function mapMachine(rates, settings, machineRow) { const deviceId = machineRow.device_id const config = settings.config @@ -69,10 +75,15 @@ function mapMachine (rates, settings, machineRow) { const lastOnline = machineRow.last_online.toISOString() const status = machineRow.stale ? 'online' : 'offline' const showLimitsAndVerification = coinAtmRadar.limitsAndVerification - const cashLimit = showLimitsAndVerification ? (_.get('threshold', complianceTriggers.getCashLimit(triggers)) || Infinity) : null + const cashLimit = showLimitsAndVerification + ? _.get('threshold', complianceTriggers.getCashLimit(triggers)) || Infinity + : null const cryptoCurrencies = locale.cryptoCurrencies const identification = mapIdentification(config) - const coins = _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies) + const coins = _.map( + _.partial(mapCoin, [rates, deviceId, settings]), + cryptoCurrencies, + ) return { machineId: deviceId, address: { @@ -80,12 +91,12 @@ function mapMachine (rates, settings, machineRow) { city: null, region: null, postalCode: null, - country: null + country: null, }, location: { name: null, url: null, - phone: null + phone: null, }, status, lastOnline, @@ -98,20 +109,21 @@ function mapMachine (rates, settings, machineRow) { cashOutDailyLimit: cashLimit, fiatCurrency: locale.fiatCurrency, identification, - coins + coins, } } -function getMachines (rates, settings) { +function getMachines(rates, settings) { const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices where display=TRUE and paired=TRUE order by created` - return db.any(sql, [STALE_INTERVAL]) + return db + .any(sql, [STALE_INTERVAL]) .then(_.map(_.partial(mapMachine, [rates, settings]))) } -function sendRadar (data) { +function sendRadar(data) { const url = COIN_ATM_RADAR_URL if (_.isEmpty(url)) { @@ -123,31 +135,32 @@ function sendRadar (data) { method: 'post', data, timeout: TIMEOUT, - maxContentLength: MAX_CONTENT_LENGTH + maxContentLength: MAX_CONTENT_LENGTH, } - return axios.default(config) - .then(r => logger.info(r.status)) + return axios.default(config).then(r => logger.info(r.status)) } -function mapRecord (rates, settings) { +function mapRecord(rates, settings) { const timestamp = new Date().toISOString() - return Promise.all([getMachines(rates, settings), getOperatorId('coinatmradar')]) - .then(([machines, operatorId]) => { - return { - operatorId: operatorId, - operator: { - name: null, - phone: null, - email: null - }, - timestamp, - machines - } - }) + return Promise.all([ + getMachines(rates, settings), + getOperatorId('coinatmradar'), + ]).then(([machines, operatorId]) => { + return { + operatorId: operatorId, + operator: { + name: null, + phone: null, + email: null, + }, + timestamp, + machines, + } + }) } -function update (rates, settings) { +function update(rates, settings) { const coinAtmRadar = configManager.getCoinAtmRadar(settings.config) if (!coinAtmRadar.active) return Promise.resolve() diff --git a/packages/server/lib/coinatmradar/test/coinatmradar.test.js b/packages/server/lib/coinatmradar/test/coinatmradar.test.js index 4e96a0db..512de274 100644 --- a/packages/server/lib/coinatmradar/test/coinatmradar.test.js +++ b/packages/server/lib/coinatmradar/test/coinatmradar.test.js @@ -80,7 +80,7 @@ const settings = { threshold: 123, id: '9c3b5af8-b1d1-4125-b169-0e913b33894c', direction: 'both', - triggerType: 'txAmount' + triggerType: 'txAmount', }, { requirement: 'sms', @@ -88,7 +88,7 @@ const settings = { thresholdDays: 1, id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b', direction: 'both', - triggerType: 'txVelocity' + triggerType: 'txVelocity', }, { requirement: 'sms', @@ -96,58 +96,58 @@ const settings = { thresholdDays: 1, id: '6ac38fe6-172c-48a4-8a7f-605213cbd600', direction: 'both', - triggerType: 'txVolume' - } + triggerType: 'txVolume', + }, ], notifications_sms_transactions: true, - notifications_highValueTransaction: 50 + notifications_highValueTransaction: 50, }, - accounts: {} + accounts: {}, } const rates = [ { rates: { ask: new BN(19164.3), - bid: new BN(19164.2) + bid: new BN(19164.2), }, - timestamp: +new Date() + timestamp: +new Date(), }, { rates: { ask: new BN(594.54), - bid: new BN(594.09) + bid: new BN(594.09), }, - timestamp: +new Date() + timestamp: +new Date(), }, { rates: { ask: new BN(84.38), - bid: new BN(84.37) + bid: new BN(84.37), }, - timestamp: +new Date() + timestamp: +new Date(), }, { rates: { ask: new BN(102.8), - bid: new BN(101.64) + bid: new BN(101.64), }, - timestamp: +new Date() + timestamp: +new Date(), }, { rates: { ask: new BN(74.91), - bid: new BN(74.12) + bid: new BN(74.12), }, - timestamp: +new Date() + timestamp: +new Date(), }, { rates: { ask: new BN(284.4), - bid: new BN(284.4) + bid: new BN(284.4), }, - timestamp: +new Date() - } + timestamp: +new Date(), + }, ] const dbResponse = [ @@ -155,32 +155,32 @@ const dbResponse = [ device_id: 'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', last_online: new Date('2020-11-16T13:11:03.169Z'), - stale: false + stale: false, }, { device_id: '9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7', last_online: new Date('2020-11-16T16:21:35.948Z'), - stale: false + stale: false, }, { device_id: '5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913', last_online: new Date('2020-11-19T15:07:57.089Z'), - stale: false + stale: false, }, { device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05', last_online: new Date('2020-11-26T20:05:57.792Z'), - stale: false + stale: false, }, { device_id: '490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88', last_online: new Date('2020-12-04T16:48:05.129Z'), - stale: false - } + stale: false, + }, ] function validateData(data) { @@ -189,7 +189,7 @@ function validateData(data) { operator: yup.object().shape({ name: yup.string().nullable(), phone: yup.string().nullable(), - email: yup.string().email().nullable() + email: yup.string().email().nullable(), }), timestamp: yup.string().required('timestamp not provided'), machines: yup.array().of( @@ -200,12 +200,12 @@ function validateData(data) { city: yup.string().nullable(), region: yup.string().nullable(), postalCode: yup.string().nullable(), - country: yup.string().nullable() + country: yup.string().nullable(), }), location: yup.object().required('location object not provided').shape({ name: yup.string().nullable(), url: yup.string().nullable(), - phone: yup.string().nullable() + phone: yup.string().nullable(), }), status: yup .string() @@ -231,7 +231,7 @@ function validateData(data) { .required('isIdDocScan boolean not defined'), isFingerprint: yup .boolean() - .required('isFingerprint boolean not defined') + .required('isFingerprint boolean not defined'), }), coins: yup.array().of( yup.object().shape({ @@ -240,11 +240,11 @@ function validateData(data) { cashOutFee: yup.number().nullable(), cashInFixedFee: yup.number().nullable(), cashInRate: yup.number().nullable(), - cashOutRate: yup.number().nullable() - }) - ) - }) - ) + cashOutRate: yup.number().nullable(), + }), + ), + }), + ), }) return schema.validate(data) } @@ -252,13 +252,13 @@ function validateData(data) { test('Verify axios request schema', async () => { const axios = require('axios') - jest.spyOn(axios, 'default').mockImplementation( - jest.fn(req => - validateData(req.data) - .then(() => ({ status: 'mock status 200' })) - .catch(e => fail(e)) + jest + .spyOn(axios, 'default') + .mockImplementation( + jest.fn(req => + validateData(req.data).then(() => ({ status: 'mock status 200' })), + ), ) - ) db.any.mockResolvedValue(dbResponse) await car.update(rates, settings) diff --git a/packages/server/lib/commission-math.js b/packages/server/lib/commission-math.js index 61103f2d..aa9079b3 100644 --- a/packages/server/lib/commission-math.js +++ b/packages/server/lib/commission-math.js @@ -2,31 +2,41 @@ const BN = require('./bn') const configManager = require('./new-config-manager') const { utils: coinUtils } = require('@lamassu/coins') -function truncateCrypto (cryptoAtoms, cryptoCode) { +function truncateCrypto(cryptoAtoms, cryptoCode) { const DECIMAL_PLACES = 6 if (cryptoAtoms.eq(0)) return cryptoAtoms const scale = coinUtils.getCryptoCurrency(cryptoCode).unitScale const scaleFactor = BN(10).pow(scale) - return new BN(cryptoAtoms).integerValue(BN.ROUND_DOWN).div(scaleFactor) - .decimalPlaces(DECIMAL_PLACES).times(scaleFactor) + return new BN(cryptoAtoms) + .integerValue(BN.ROUND_DOWN) + .div(scaleFactor) + .decimalPlaces(DECIMAL_PLACES) + .times(scaleFactor) } -function fiatToCrypto (tx, rec, deviceId, config) { +function fiatToCrypto(tx, rec, deviceId, config) { const usableFiat = rec.fiat - rec.cashInFee - const commissions = configManager.getCommissions(tx.cryptoCode, deviceId, config) + const commissions = configManager.getCommissions( + tx.cryptoCode, + deviceId, + config, + ) const tickerRate = new BN(tx.rawTickerPrice) const discount = getDiscountRate(tx.discount, commissions[tx.direction]) const rate = tickerRate.times(discount).decimalPlaces(5) const unitScale = coinUtils.getCryptoCurrency(tx.cryptoCode).unitScale const unitScaleFactor = new BN(10).pow(unitScale) - return truncateCrypto(new BN(usableFiat).div(rate.div(unitScaleFactor)), tx.cryptoCode) + return truncateCrypto( + new BN(usableFiat).div(rate.div(unitScaleFactor)), + tx.cryptoCode, + ) } -function getDiscountRate (discount, commission) { +function getDiscountRate(discount, commission) { const bnDiscount = discount ? new BN(discount) : new BN(0) const bnCommission = new BN(commission) const percentageDiscount = new BN(1).minus(bnDiscount.div(100)) @@ -36,5 +46,5 @@ function getDiscountRate (discount, commission) { module.exports = { fiatToCrypto, - getDiscountRate + getDiscountRate, } diff --git a/packages/server/lib/compliance-external.js b/packages/server/lib/compliance-external.js index 466b77e9..422a4081 100644 --- a/packages/server/lib/compliance-external.js +++ b/packages/server/lib/compliance-external.js @@ -8,20 +8,25 @@ const getPlugin = (settings, pluginCode) => { const account = settings.accounts[pluginCode] const plugin = ph.load(ph.COMPLIANCE, pluginCode) - return ({ plugin, account }) + return { plugin, account } } const getStatus = (settings, service, customerId) => { try { const { plugin, account } = getPlugin(settings, service) - return plugin.getApplicantStatus(account, customerId) - .then((status) => ({ + return plugin + .getApplicantStatus(account, customerId) + .then(status => ({ service, - status + status, })) - .catch((error) => { - if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message) + .catch(error => { + if (error.response.status !== 404) + logger.error( + `Error getting applicant for service ${service}:`, + error.message, + ) return { service: service, status: null, @@ -34,28 +39,22 @@ const getStatus = (settings, service, customerId) => { status: null, }) } - } const getStatusMap = (settings, customerExternalCompliance) => { const triggers = configManager.getTriggers(settings.config) - const services = _.flow( - _.map('externalService'), - _.compact, - _.uniq - )(triggers) + const services = _.flow(_.map('externalService'), _.compact, _.uniq)(triggers) const applicantPromises = _.map(service => { return getStatus(settings, service, customerExternalCompliance) })(services) - return Promise.all(applicantPromises) - .then((applicantResults) => { - return _.reduce((map, result) => { - if (result.status) map[result.service] = result.status - return map - }, {})(applicantResults) - }) + return Promise.all(applicantPromises).then(applicantResults => { + return _.reduce((map, result) => { + if (result.status) map[result.service] = result.status + return map + }, {})(applicantResults) + }) } const createApplicant = (settings, externalService, customerId) => { @@ -76,5 +75,5 @@ module.exports = { getStatusMap, getStatus, createApplicant, - createLink + createLink, } diff --git a/packages/server/lib/compliance-triggers.js b/packages/server/lib/compliance-triggers.js index dcbe6ab2..7bb75b0c 100644 --- a/packages/server/lib/compliance-triggers.js +++ b/packages/server/lib/compliance-triggers.js @@ -1,26 +1,35 @@ const _ = require('lodash/fp') -function getBackwardsCompatibleTriggers (triggers) { - const filtered = _.filter(_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }))(triggers) +function getBackwardsCompatibleTriggers(triggers) { + const filtered = _.filter( + _.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }), + )(triggers) const grouped = _.groupBy(_.prop('requirement'))(filtered) - return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped) + return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))( + grouped, + ) } -function hasSanctions (triggers) { +function hasSanctions(triggers) { return _.some(_.matches({ requirement: 'sanctions' }))(triggers) } -function maxDaysThreshold (triggers) { +function maxDaysThreshold(triggers) { return _.max(_.map('thresholdDays')(triggers)) } -function getCashLimit (triggers) { - const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount'])) - const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend'])) +function getCashLimit(triggers) { + const withFiat = _.filter(({ triggerType }) => + _.includes(triggerType, ['txVolume', 'txAmount']), + ) + const blocking = _.filter(({ requirement }) => + _.includes(requirement, ['block', 'suspend']), + ) return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers) } -const hasRequirement = requirement => _.compose(_.negate(_.isEmpty), _.find(_.matches({ requirement }))) +const hasRequirement = requirement => + _.compose(_.negate(_.isEmpty), _.find(_.matches({ requirement }))) const hasPhone = hasRequirement('sms') const hasFacephoto = hasRequirement('facephoto') @@ -28,7 +37,16 @@ const hasIdScan = hasRequirement('idCardData') const AUTH_METHODS = { SMS: 'SMS', - EMAIL: 'EMAIL' + EMAIL: 'EMAIL', } -module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS } \ No newline at end of file +module.exports = { + getBackwardsCompatibleTriggers, + hasSanctions, + maxDaysThreshold, + getCashLimit, + hasPhone, + hasFacephoto, + hasIdScan, + AUTH_METHODS, +} diff --git a/packages/server/lib/compliance.js b/packages/server/lib/compliance.js index ddb27065..ab3a6343 100644 --- a/packages/server/lib/compliance.js +++ b/packages/server/lib/compliance.js @@ -5,75 +5,87 @@ const logger = require('./logger') const db = require('./db') const ofac = require('./ofac/index') -function logSanctionsMatch (deviceId, customer, sanctionsId, alias) { +function logSanctionsMatch(deviceId, customer, sanctionsId, alias) { const sql = `insert into sanctions_logs (id, device_id, sanctioned_id, sanctioned_alias_id, sanctioned_alias_full_name, customer_id) values ($1, $2, $3, $4, $5, $6)` - return db.none(sql, [uuid.v4(), deviceId, sanctionsId, alias.id, alias.fullName, customer.id]) + return db.none(sql, [ + uuid.v4(), + deviceId, + sanctionsId, + alias.id, + alias.fullName, + customer.id, + ]) } -function logSanctionsMatches (deviceId, customer, results) { - const logAlias = resultId => alias => logSanctionsMatch(deviceId, customer, resultId, alias) +function logSanctionsMatches(deviceId, customer, results) { + const logAlias = resultId => alias => + logSanctionsMatch(deviceId, customer, resultId, alias) const logResult = result => _.map(logAlias(result.id), result.aliases) return Promise.all(_.flatMap(logResult, results)) } -function matchOfac (deviceId, customer) { - return Promise.resolve() - .then(() => { - // Probably because we haven't asked for ID yet - if (!_.isPlainObject(customer.idCardData)) { - return true - } +function matchOfac(deviceId, customer) { + return Promise.resolve().then(() => { + // Probably because we haven't asked for ID yet + if (!_.isPlainObject(customer.idCardData)) { + return true + } - const nameParts = { - firstName: customer.idCardData.firstName, - lastName: customer.idCardData.lastName - } + const nameParts = { + firstName: customer.idCardData.firstName, + lastName: customer.idCardData.lastName, + } - if (_.some(_.isNil, _.values(nameParts))) { - logger.error(new Error(`Insufficient idCardData while matching OFAC for: ${customer.id}`)) - return true - } + if (_.some(_.isNil, _.values(nameParts))) { + logger.error( + new Error( + `Insufficient idCardData while matching OFAC for: ${customer.id}`, + ), + ) + return true + } - const birthDate = customer.idCardData.dateOfBirth + const birthDate = customer.idCardData.dateOfBirth - if (_.isNil(birthDate)) { - logger.error(new Error(`No birth date while matching OFAC for: ${customer.id}`)) - return true - } + if (_.isNil(birthDate)) { + logger.error( + new Error(`No birth date while matching OFAC for: ${customer.id}`), + ) + return true + } - const options = { - threshold: 0.85, - fullNameThreshold: 0.95, - debug: false - } + const options = { + threshold: 0.85, + fullNameThreshold: 0.95, + debug: false, + } - const results = ofac.match(nameParts, birthDate, options) + const results = ofac.match(nameParts, birthDate, options) - return logSanctionsMatches(deviceId, customer, results) - .then(() => !_.isEmpty(results)) - }) + return logSanctionsMatches(deviceId, customer, results).then( + () => !_.isEmpty(results), + ) + }) } -function validateOfac (deviceId, customer) { +function validateOfac(deviceId, customer) { if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false) if (customer.sanctionsOverride === 'verified') return Promise.resolve(true) - return matchOfac(deviceId, customer) - .then(didMatch => !didMatch) + return matchOfac(deviceId, customer).then(didMatch => !didMatch) } -function validationPatch (deviceId, customer) { - return validateOfac(deviceId, customer) - .then(sanctions => - _.isNil(customer.sanctions) || customer.sanctions !== sanctions ? - { sanctions } : - {} - ) +function validationPatch(deviceId, customer) { + return validateOfac(deviceId, customer).then(sanctions => + _.isNil(customer.sanctions) || customer.sanctions !== sanctions + ? { sanctions } + : {}, + ) } -module.exports = {validationPatch} +module.exports = { validationPatch } diff --git a/packages/server/lib/compliance_overrides.js b/packages/server/lib/compliance_overrides.js index f84fe0d1..a70c1948 100644 --- a/packages/server/lib/compliance_overrides.js +++ b/packages/server/lib/compliance_overrides.js @@ -11,7 +11,7 @@ const uuid = require('uuid') * * @returns {object} Newly created compliance override */ -function add (complianceOverride) { +function add(complianceOverride) { const sql = `insert into compliance_overrides (id, customer_id, @@ -25,7 +25,7 @@ function add (complianceOverride) { complianceOverride.customerId, complianceOverride.complianceType, complianceOverride.overrideBy, - complianceOverride.verification + complianceOverride.verification, ]) } diff --git a/packages/server/lib/constants.js b/packages/server/lib/constants.js index 07c2ad84..64f98687 100644 --- a/packages/server/lib/constants.js +++ b/packages/server/lib/constants.js @@ -10,43 +10,43 @@ const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HO const anonymousCustomer = { uuid: '47ac1184-8102-11e7-9079-8f13a7117867', - name: 'anonymous' + name: 'anonymous', } const CASH_UNIT_CAPACITY = { default: { cashbox: 600, - cassette: 500 + cassette: 500, }, douro: { cashbox: 600, - cassette: 500 + cassette: 500, }, grandola: { cashbox: 2000, - recycler: 2800 + recycler: 2800, }, aveiro: { cashbox: 1500, recycler: 60, - cassette: 500 + cassette: 500, }, tejo: { // TODO: add support for the different cashbox configuration in Tejo cashbox: 1000, - cassette: 500 + cassette: 500, }, gaia: { - cashbox: 600 + cashbox: 600, }, sintra: { cashbox: 1000, - cassette: 500 + cassette: 500, }, gmuk1: { cashbox: 2200, - cassette: 2000 - } + cassette: 2000, + }, } const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2 @@ -69,7 +69,7 @@ const WALLET_SCORE_THRESHOLD = 9 const BALANCE_FETCH_SPEED_MULTIPLIER = { NORMAL: 1, - SLOW: 3 + SLOW: 3, } module.exports = { @@ -90,5 +90,5 @@ module.exports = { WALLET_SCORE_THRESHOLD, RECEIPT, PSQL_URL, - BALANCE_FETCH_SPEED_MULTIPLIER + BALANCE_FETCH_SPEED_MULTIPLIER, } diff --git a/packages/server/lib/customer-notes.js b/packages/server/lib/customer-notes.js index b9e4ae81..cfe92da5 100644 --- a/packages/server/lib/customer-notes.js +++ b/packages/server/lib/customer-notes.js @@ -5,7 +5,9 @@ const db = require('./db') const getCustomerNotes = customerId => { const sql = `SELECT * FROM customer_notes WHERE customer_id=$1` - return db.oneOrNone(sql, [customerId]).then(res => _.mapKeys((_, key) => _.camelize(key), res)) + return db + .oneOrNone(sql, [customerId]) + .then(res => _.mapKeys((_, key) => _.camelize(key), res)) } const createCustomerNote = (customerId, userId, title, content) => { @@ -27,5 +29,5 @@ module.exports = { getCustomerNotes, createCustomerNote, deleteCustomerNote, - updateCustomerNote + updateCustomerNote, } diff --git a/packages/server/lib/customers.js b/packages/server/lib/customers.js index d53534a3..706ad860 100644 --- a/packages/server/lib/customers.js +++ b/packages/server/lib/customers.js @@ -21,7 +21,11 @@ const externalCompliance = require('./compliance-external') const { APPROVED, RETRY } = require('./plugins/compliance/consts') -const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'walletScoringError'] +const TX_PASSTHROUGH_ERROR_CODES = [ + 'operatorCancel', + 'scoreThresholdReached', + 'walletScoringError', +] const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR @@ -37,16 +41,16 @@ const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR * * @returns {object} Newly created customer */ -function add (customer) { - const sql = 'insert into customers (id, phone, phone_at) values ($1, $2, now()) returning *' - return db.one(sql, [uuid.v4(), customer.phone]) - .then(camelize) +function add(customer) { + const sql = + 'insert into customers (id, phone, phone_at) values ($1, $2, now()) returning *' + return db.one(sql, [uuid.v4(), customer.phone]).then(camelize) } -function addWithEmail (customer) { - const sql = 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *' - return db.one(sql, [uuid.v4(), customer.email]) - .then(camelize) +function addWithEmail(customer) { + const sql = + 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *' + return db.one(sql, [uuid.v4(), customer.email]).then(camelize) } /** @@ -60,16 +64,14 @@ function addWithEmail (customer) { * * @returns {object} Customer */ -function get (phone) { +function get(phone) { const sql = 'select * from customers where phone=$1' - return db.oneOrNone(sql, [phone]) - .then(camelize) + return db.oneOrNone(sql, [phone]).then(camelize) } -function getWithEmail (email) { +function getWithEmail(email) { const sql = 'select * from customers where email=$1' - return db.oneOrNone(sql, [email]) - .then(camelize) + return db.oneOrNone(sql, [email]).then(camelize) } /** @@ -84,16 +86,20 @@ function getWithEmail (email) { * * @returns {Promise} Newly updated Customer */ -function update (id, data, userToken) { +function update(id, data, userToken) { const formattedData = _.omit(['id'], _.mapKeys(_.snakeCase, data)) - const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken)) + const enhancedUpdateData = enhanceAtFields( + enhanceOverrideFields(formattedData, userToken), + ) const updateData = updateOverride(enhancedUpdateData) - const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + + const sql = + Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + ' where id=$1 returning *' - return db.one(sql, [id]) + return db + .one(sql, [id]) .then(assignCustomerData) .then(addComplianceOverrides(id, updateData, userToken)) .then(getCustomInfoRequestsData) @@ -111,7 +117,7 @@ function update (id, data, userToken) { * * @returns {Promise} Newly updated Customer */ -async function updateCustomer (id, data, userToken) { +async function updateCustomer(id, data, userToken) { const formattedData = _.pick( [ 'sanctions', @@ -123,16 +129,20 @@ async function updateCustomer (id, data, userToken) { 'sanctions_override', 'front_camera_override', 'suspended_until', - 'phone_override' + 'phone_override', ], - _.mapKeys(_.snakeCase, data)) + _.mapKeys(_.snakeCase, data), + ) - const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken)) + const enhancedUpdateData = enhanceAtFields( + enhanceOverrideFields(formattedData, userToken), + ) const updateData = updateOverride(enhancedUpdateData) - + if (!_.isEmpty(updateData)) { - const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + - ' where id=$1' + const sql = + Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + + ' where id=$1' await db.none(sql, [id]) } @@ -153,32 +163,38 @@ async function updateCustomer (id, data, userToken) { * @returns {Promise} Newly updated Customer */ -function edit (id, data, userToken) { +function edit(id, data, userToken) { const defaults = [ 'front_camera', 'id_card_data', 'id_card_photo', 'us_ssn', 'subscriber_info', - 'name' + 'name', ] - const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data))) + const filteredData = _.pick( + defaults, + _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)), + ) if (_.isEmpty(filteredData)) return getCustomerById(id) - const formattedData = enhanceEditedPhotos(enhanceEditedFields(filteredData, userToken)) + const formattedData = enhanceEditedPhotos( + enhanceEditedFields(filteredData, userToken), + ) const defaultDbData = { customer_id: id, created: new Date(), - ...formattedData + ...formattedData, } - const cs = new Pgp.helpers.ColumnSet(_.keys(defaultDbData), - { table: 'edited_customer_data' }) - const onConflict = ' ON CONFLICT (customer_id) DO UPDATE SET ' + + const cs = new Pgp.helpers.ColumnSet(_.keys(defaultDbData), { + table: 'edited_customer_data', + }) + const onConflict = + ' ON CONFLICT (customer_id) DO UPDATE SET ' + cs.assignColumns({ from: 'EXCLUDED', skip: ['customer_id', 'created'] }) const upsert = Pgp.helpers.insert(defaultDbData, cs) + onConflict - return db.none(upsert) - .then(getCustomerById(id)) + return db.none(upsert).then(getCustomerById(id)) } /** @@ -193,9 +209,9 @@ function edit (id, data, userToken) { * @returns {object} fields enhanced with *_by and *_at fields */ -function enhanceEditedFields (fields, userToken) { +function enhanceEditedFields(fields, userToken) { if (!userToken) return fields - _.mapKeys((field) => { + _.mapKeys(field => { fields[field + '_by'] = userToken fields[field + '_at'] = 'now()^' }, fields) @@ -212,8 +228,8 @@ function enhanceEditedFields (fields, userToken) { * @returns {object} fields enhanced with *_path */ -function enhanceEditedPhotos (fields) { - return _.mapKeys((field) => { +function enhanceEditedPhotos(fields) { + return _.mapKeys(field => { if (_.includes(field, ['front_camera', 'id_card_photo'])) { return field + '_path' } @@ -234,7 +250,7 @@ function enhanceEditedPhotos (fields) { * */ -function deleteEditedData (id, data) { +function deleteEditedData(id, data) { // TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION const defaults = [ 'front_camera', @@ -242,13 +258,14 @@ function deleteEditedData (id, data) { 'id_card_photo', 'us_ssn', 'subscriber_info', - 'name' + 'name', ] const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data)) if (_.isEmpty(filteredData)) return getCustomerById(id) - const cs = new Pgp.helpers.ColumnSet(_.keys(filteredData), - { table: 'edited_customer_data' }) + const cs = new Pgp.helpers.ColumnSet(_.keys(filteredData), { + table: 'edited_customer_data', + }) const update = Pgp.helpers.update(filteredData, cs) db.none(update) return getCustomerById(id) @@ -267,9 +284,10 @@ function deleteEditedData (id, data) { * @returns {object} path New photo path * */ -async function updateEditedPhoto (id, photo, photoType) { +async function updateEditedPhoto(id, photo, photoType) { const newPatch = {} - const baseDir = photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR + const baseDir = + photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR const { createReadStream, filename } = photo const stream = createReadStream() @@ -297,11 +315,6 @@ const invalidateCustomerNotifications = (id, data) => { return notifierQueries.invalidateNotification(detailB, 'compliance') } -const updateSubscriberData = (customerId, data, userToken) => { - const sql = `UPDATE customers SET subscriber_info=$1, subscriber_info_at=now(), subscriber_info_by=$2 WHERE id=$3` - return db.none(sql, [data, userToken, customerId]) -} - /** * Get customer by id * @@ -315,9 +328,10 @@ const updateSubscriberData = (customerId, data, userToken) => { * * Used for the machine. */ -function getById (id) { +function getById(id) { const sql = 'select * from customers where id=$1' - return db.oneOrNone(sql, [id]) + return db + .oneOrNone(sql, [id]) .then(assignCustomerData) .then(getCustomInfoRequestsData) .then(getExternalComplianceMachine) @@ -334,19 +348,16 @@ function getById (id) { * @param {object} customer Customer with snake_case fields * @returns {object} Camelized Customer object */ -function camelize (customer) { +function camelize(customer) { return customer ? _.mapKeys(_.camelCase, customer) : null } -function camelizeDeep (customer) { - return _.flow( - camelize, - it => ({ - ...it, - notes: (it.notes ?? []).map(camelize), - externalCompliance: (it.externalCompliance ?? []).map(camelize) - }) - )(customer) +function camelizeDeep(customer) { + return _.flow(camelize, it => ({ + ...it, + notes: (it.notes ?? []).map(camelize), + externalCompliance: (it.externalCompliance ?? []).map(camelize), + }))(customer) } /** @@ -358,7 +369,7 @@ function camelizeDeep (customer) { * * @returns {array} Array of compliance types' names */ -function getComplianceTypes () { +function getComplianceTypes() { return [ 'sms', 'email', @@ -367,35 +378,40 @@ function getComplianceTypes () { 'front_camera', 'sanctions', 'authorized', - 'us_ssn' ] + 'us_ssn', + ] } -function updateOverride (fields) { +function updateOverride(fields) { const updateableFields = [ 'id_card_data', 'id_card_photo_path', 'front_camera_path', 'authorized', - 'us_ssn' + 'us_ssn', ] const removePathSuffix = _.map(_.replace('_path', '')) const getPairs = _.map(f => [`${f}_override`, 'automatic']) const updatedFields = _.intersection(updateableFields, _.keys(fields)) - const overrideFields = _.compose(_.fromPairs, getPairs, removePathSuffix)(updatedFields) + const overrideFields = _.compose( + _.fromPairs, + getPairs, + removePathSuffix, + )(updatedFields) return _.merge(fields, overrideFields) } -function enhanceAtFields (fields) { +function enhanceAtFields(fields) { const updateableFields = [ 'id_card_data', 'id_card_photo', 'front_camera', 'sanctions', 'authorized', - 'us_ssn' + 'us_ssn', ] const updatedFields = _.intersection(updateableFields, _.keys(fields)) @@ -415,17 +431,21 @@ function enhanceAtFields (fields) { * @param {string} userToken Acting user's token * @returns {object} fields enhanced with *_by and *_at fields */ -function enhanceOverrideFields (fields, userToken) { +function enhanceOverrideFields(fields, userToken) { if (!userToken) return fields // Populate with computedFields (user who overrode and overridden timestamps date) - return _.reduce(_.assign, fields, _.map((type) => { - return (fields[type + '_override']) - ? { - [type + '_override_by']: userToken, - [type + '_override_at']: 'now()^' - } - : {} - }, getComplianceTypes())) + return _.reduce( + _.assign, + fields, + _.map(type => { + return fields[type + '_override'] + ? { + [type + '_override_by']: userToken, + [type + '_override_at']: 'now()^', + } + : {} + }, getComplianceTypes()), + ) } /** @@ -443,24 +463,26 @@ function enhanceOverrideFields (fields, userToken) { * * @returns {promise} Result from compliance_overrides creation */ -function addComplianceOverrides (id, customer, userToken) { +function addComplianceOverrides(id, customer, userToken) { // Prepare compliance overrides to save const overrides = _.map(field => { const complianceName = field + '_override' - return (customer[complianceName]) ? { - customerId: id, - complianceType: field, - overrideBy: userToken, - verification: customer[complianceName] - } : null + return customer[complianceName] + ? { + customerId: id, + complianceType: field, + overrideBy: userToken, + verification: customer[complianceName], + } + : null }, getComplianceTypes()) // Save all the updated override fields - return Promise.all(_.map(complianceOverrides.add, _.compact(overrides))) - .then(() => customer) + return Promise.all(_.map(complianceOverrides.add, _.compact(overrides))).then( + () => customer, + ) } - /** * Query all customers * @@ -470,23 +492,24 @@ function addComplianceOverrides (id, customer, userToken) { * * @returns {array} Array of customers populated with status field */ -function batch () { +function batch() { const sql = `select * from customers where id != $1 order by created desc limit $2` - return db.any(sql, [ anonymous.uuid, NUM_RESULTS ]) - .then(customers => Promise.all(_.map(customer => { - return getCustomInfoRequestsData(customer) - .then(camelize) - }, customers))) + return db.any(sql, [anonymous.uuid, NUM_RESULTS]).then(customers => + Promise.all( + _.map(customer => { + return getCustomInfoRequestsData(customer).then(camelize) + }, customers), + ), + ) } -function getSlimCustomerByIdBatch (ids) { +function getSlimCustomerByIdBatch(ids) { const sql = `SELECT id, phone, id_card_data FROM customers WHERE id = ANY($1::uuid[])` - return db.any(sql, [ids]) - .then(customers => _.map(camelize, customers)) + return db.any(sql, [ids]).then(customers => _.map(camelize, customers)) } // TODO: getCustomersList and getCustomerById are very similar, so this should be refactored @@ -499,8 +522,17 @@ function getSlimCustomerByIdBatch (ids) { * @returns {array} Array of customers with it's transactions aggregations */ -function getCustomersList (phone = null, name = null, address = null, id = null, email = null) { - const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') +function getCustomersList( + phone = null, + name = null, + address = null, + id = null, + email = null, +) { + const passableErrorCodes = _.map( + Pgp.as.text, + TX_PASSTHROUGH_ERROR_CODES, + ).join(',') const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override, phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, @@ -543,11 +575,24 @@ function getCustomersList (phone = null, name = null, address = null, id = null, AND ($8 IS NULL OR email = $8) ORDER BY last_active DESC limit $3` - return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id, email ]) - .then(customers => Promise.all(_.map(customer => - getCustomInfoRequestsData(customer) - .then(camelizeDeep), customers) - ) + return db + .any(sql, [ + passableErrorCodes, + anonymous.uuid, + NUM_RESULTS, + phone, + name, + address, + id, + email, + ]) + .then(customers => + Promise.all( + _.map( + customer => getCustomInfoRequestsData(customer).then(camelizeDeep), + customers, + ), + ), ) } @@ -560,8 +605,11 @@ function getCustomersList (phone = null, name = null, address = null, id = null, * * Used for the server. */ -function getCustomerById (id) { - const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') +function getCustomerById(id) { + const passableErrorCodes = _.map( + Pgp.as.text, + TX_PASSTHROUGH_ERROR_CODES, + ).join(',') const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override, phone, phone_at, email, email_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at, @@ -596,7 +644,8 @@ function getCustomerById (id) { ) cn ON c.id = cn.customer_id WHERE c.id = $2 ) AS cl WHERE rn = 1` - return db.oneOrNone(sql, [passableErrorCodes, id]) + return db + .oneOrNone(sql, [passableErrorCodes, id]) .then(assignCustomerData) .then(getCustomInfoRequestsData) .then(getExternalCompliance) @@ -604,16 +653,17 @@ function getCustomerById (id) { .then(formatSubscriberInfo) } -function assignCustomerData (customer) { - return getEditedData(customer.id) - .then(customerEditedData => selectLatestData(customer, customerEditedData)) +function assignCustomerData(customer) { + return getEditedData(customer.id).then(customerEditedData => + selectLatestData(customer, customerEditedData), + ) } function formatSubscriberInfo(customer) { const subscriberInfo = customer.subscriberInfo - if(!subscriberInfo) return customer + if (!subscriberInfo) return customer const result = subscriberInfo.result - if(_.isEmpty(result)) return _.omit(['subscriberInfo'], customer) + if (_.isEmpty(result)) return _.omit(['subscriberInfo'], customer) const name = _.get('belongs_to.name')(result) const street = _.get('current_addresses[0].street_line_1')(result) @@ -623,7 +673,7 @@ function formatSubscriberInfo(customer) { customer.subscriberInfo = { name, - address: `${street ?? ''} ${city ?? ''}${street || city ? ',' : ''} ${stateCode ?? ''} ${postalCode ?? ''}` + address: `${street ?? ''} ${city ?? ''}${street || city ? ',' : ''} ${stateCode ?? ''} ${postalCode ?? ''}`, } return customer @@ -636,33 +686,32 @@ function formatSubscriberInfo(customer) { * * @returns {array} A single customer instance with the most recent edited data */ -function getEditedData (id) { +function getEditedData(id) { const sql = `SELECT * FROM edited_customer_data WHERE customer_id = $1` - return db.oneOrNone(sql, [id]) - .then(_.omitBy(_.isNil)) + return db.oneOrNone(sql, [id]).then(_.omitBy(_.isNil)) } -function selectLatestData (customerData, customerEditedData) { +function selectLatestData(customerData, customerEditedData) { const defaults = [ 'front_camera', 'id_card_data', 'id_card_photo', 'us_ssn', 'subscriber_info', - 'name' + 'name', ] _.map(field => { const atField = field + '_at' const byField = field + '_by' - if (_.includes(field, ['front_camera', 'id_card_photo'])) field = field + '_path' + if (_.includes(field, ['front_camera', 'id_card_photo'])) + field = field + '_path' if (!_.has(field, customerData) || !_.has(field, customerEditedData)) return if (customerData[atField] < customerEditedData[atField]) { customerData[field] = customerEditedData[field] customerData[atField] = customerEditedData[atField] customerData[byField] = customerEditedData[byField] } - } - , defaults) + }, defaults) return customerData } @@ -671,54 +720,52 @@ function selectLatestData (customerData, customerEditedData) { * @param {Object} patch customer update record * @returns {Promise} new patch to be applied */ -function updatePhotoCard (id, patch) { - return Promise.resolve(patch) - .then(patch => { - // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. - const imageData = _.get('idCardPhotoData', patch) +function updatePhotoCard(id, patch) { + return Promise.resolve(patch).then(patch => { + // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. + const imageData = _.get('idCardPhotoData', patch) - if (_.isEmpty(imageData)) { - return patch - } + if (_.isEmpty(imageData)) { + return patch + } - // remove idCardPhotoData from the update record - const newPatch = _.omit('idCardPhotoData', patch) + // remove idCardPhotoData from the update record + const newPatch = _.omit('idCardPhotoData', patch) - // decode the base64 string to binary data - const decodedImageData = Buffer.from(imageData, 'base64') + // decode the base64 string to binary data + const decodedImageData = Buffer.from(imageData, 'base64') - // workout the image hash - // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 - const hash = crypto - .createHash('sha256') - .update(imageData) - .digest('hex') + // workout the image hash + // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 + const hash = crypto.createHash('sha256').update(imageData).digest('hex') - // workout the image folder - // i.e. 24/0e/85 - const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) + // workout the image folder + // i.e. 24/0e/85 + const rpath = _.join( + path.sep, + _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), + ) - // i.e. ..//idphotocard/24/0e/85 - const dirname = path.join(ID_PHOTO_CARD_DIR, rpath) + // i.e. ..//idphotocard/24/0e/85 + const dirname = path.join(ID_PHOTO_CARD_DIR, rpath) - // create the directory tree if needed - _.attempt(() => makeDir.sync(dirname)) + // create the directory tree if needed + _.attempt(() => makeDir.sync(dirname)) - // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg - const filename = path.join(dirname, hash + '.jpg') + // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg + const filename = path.join(dirname, hash + '.jpg') - // update db record patch - // i.e. { - // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", - // "idCardPhotoAt": "now()" - // } - newPatch.idCardPhotoPath = path.join(rpath, hash + '.jpg') - newPatch.idCardPhotoAt = 'now()' + // update db record patch + // i.e. { + // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", + // "idCardPhotoAt": "now()" + // } + newPatch.idCardPhotoPath = path.join(rpath, hash + '.jpg') + newPatch.idCardPhotoAt = 'now()' - // write image file - return writeFile(filename, decodedImageData) - .then(() => newPatch) - }) + // write image file + return writeFile(filename, decodedImageData).then(() => newPatch) + }) } /** @@ -726,32 +773,30 @@ function updatePhotoCard (id, patch) { * @param {String} directory directory path of id card data for a certain user */ -function updatePhotos (imagesData, id, dir) { - return Promise.resolve(imagesData) - .then(imagesData => { - const newPatch = {} - if (_.isEmpty(imagesData)) { - return newPatch - } - // i.e. ..////idcarddata - const dirname = path.join(dir) - // create the directory tree if needed - _.attempt(() => makeDir.sync(dirname)) - const promises = imagesData.map((imageData, index) => { - // decode the base64 string to binary data - const decodedImageData = Buffer.from(imageData, 'base64') - // i.e. ..////idcarddata/1.jpg - const filename = path.join(dirname, index + '.jpg') - return writeFile(filename, decodedImageData) - }) - - return Promise.all(promises) - .then(arr => { - newPatch.idCardData = path.join(dirname) - newPatch.idCardDataAt = 'now()' - return newPatch - }) +function updatePhotos(imagesData, id, dir) { + return Promise.resolve(imagesData).then(imagesData => { + const newPatch = {} + if (_.isEmpty(imagesData)) { + return newPatch + } + // i.e. ..////idcarddata + const dirname = path.join(dir) + // create the directory tree if needed + _.attempt(() => makeDir.sync(dirname)) + const promises = imagesData.map((imageData, index) => { + // decode the base64 string to binary data + const decodedImageData = Buffer.from(imageData, 'base64') + // i.e. ..////idcarddata/1.jpg + const filename = path.join(dirname, index + '.jpg') + return writeFile(filename, decodedImageData) }) + + return Promise.all(promises).then(() => { + newPatch.idCardData = path.join(dirname) + newPatch.idCardDataAt = 'now()' + return newPatch + }) + }) } /** @@ -759,192 +804,218 @@ function updatePhotos (imagesData, id, dir) { * @param {Object} patch customer latest id card photos * @returns {Promise} new patch to be applied */ -function updateIdCardData (patch, id) { +function updateIdCardData(patch, id) { /* TODO: fetch operator id */ const operatorId = 'id-operator' const directory = `${OPERATOR_DATA_DIR}/${operatorId}/${id}/` - return Promise.resolve(patch) - .then(patch => { - const imagesData = _.get('photos', patch) - return updatePhotos(imagesData, id, directory) - .catch(err => logger.error('while saving the image: ', err)) - }) + return Promise.resolve(patch).then(patch => { + const imagesData = _.get('photos', patch) + return updatePhotos(imagesData, id, directory).catch(err => + logger.error('while saving the image: ', err), + ) + }) } /** * @param {String} imageData customer t&c photo data * @returns {Promise} new patch to be applied */ -function updateTxCustomerPhoto (imageData) { - return Promise.resolve(imageData) - .then(imageData => { - const newPatch = {} - const directory = `${OPERATOR_DATA_DIR}/customersphotos` +function updateTxCustomerPhoto(imageData) { + return Promise.resolve(imageData).then(imageData => { + const newPatch = {} + const directory = `${OPERATOR_DATA_DIR}/customersphotos` - if (_.isEmpty(imageData)) { - return - } + if (_.isEmpty(imageData)) { + return + } - // decode the base64 string to binary data - const decodedImageData = Buffer.from(imageData, 'base64') + // decode the base64 string to binary data + const decodedImageData = Buffer.from(imageData, 'base64') - // workout the image hash - // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 - const hash = crypto - .createHash('sha256') - .update(imageData) - .digest('hex') + // workout the image hash + // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 + const hash = crypto.createHash('sha256').update(imageData).digest('hex') - // workout the image folder - // i.e. 24/0e/85 - const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) + // workout the image folder + // i.e. 24/0e/85 + const rpath = _.join( + path.sep, + _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), + ) - // i.e. ..///customersphotos/24/0e/85 - const dirname = path.join(directory, rpath) + // i.e. ..///customersphotos/24/0e/85 + const dirname = path.join(directory, rpath) - // create the directory tree if needed - _.attempt(() => makeDir.sync(dirname)) + // create the directory tree if needed + _.attempt(() => makeDir.sync(dirname)) - // i.e. ..///customersphotos/24/0e/85/240e85ff2e4bb931f235985dd01....jpg - const filename = path.join(dirname, hash + '.jpg') + // i.e. ..///customersphotos/24/0e/85/240e85ff2e4bb931f235985dd01....jpg + const filename = path.join(dirname, hash + '.jpg') - // update db record patch - // i.e. { - // "idCustomerTxPhoto": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", - // "idCustomerTxPhotoAt": "now()" - // } - newPatch.txCustomerPhotoPath = path.join(rpath, hash + '.jpg') - newPatch.txCustomerPhotoAt = 'now()' + // update db record patch + // i.e. { + // "idCustomerTxPhoto": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", + // "idCustomerTxPhotoAt": "now()" + // } + newPatch.txCustomerPhotoPath = path.join(rpath, hash + '.jpg') + newPatch.txCustomerPhotoAt = 'now()' - // write image file - return writeFile(filename, decodedImageData) - .then(() => newPatch) - }) + // write image file + return writeFile(filename, decodedImageData).then(() => newPatch) + }) } -function updateFrontCamera (id, patch) { - return Promise.resolve(patch) - .then(patch => { - // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. - const imageData = _.get('frontCameraData', patch) +function updateFrontCamera(id, patch) { + return Promise.resolve(patch).then(patch => { + // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. + const imageData = _.get('frontCameraData', patch) - if (_.isEmpty(imageData)) { - return patch - } + if (_.isEmpty(imageData)) { + return patch + } - // remove idCardPhotoData from the update record - const newPatch = _.omit('frontCameraData', patch) + // remove idCardPhotoData from the update record + const newPatch = _.omit('frontCameraData', patch) - // decode the base64 string to binary data - const decodedImageData = Buffer.from(imageData, 'base64') + // decode the base64 string to binary data + const decodedImageData = Buffer.from(imageData, 'base64') - // workout the image hash - // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 - const hash = crypto - .createHash('sha256') - .update(imageData) - .digest('hex') + // workout the image hash + // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 + const hash = crypto.createHash('sha256').update(imageData).digest('hex') - // workout the image folder - // i.e. 24/0e/85 - const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash))))) + // workout the image folder + // i.e. 24/0e/85 + const rpath = _.join( + path.sep, + _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), + ) - // i.e. ..//idphotocard/24/0e/85 - const dirname = path.join(FRONT_CAMERA_DIR, rpath) + // i.e. ..//idphotocard/24/0e/85 + const dirname = path.join(FRONT_CAMERA_DIR, rpath) - // create the directory tree if needed - _.attempt(() => makeDir.sync(dirname)) + // create the directory tree if needed + _.attempt(() => makeDir.sync(dirname)) - // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg - const filename = path.join(dirname, hash + '.jpg') + // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg + const filename = path.join(dirname, hash + '.jpg') - // update db record patch - // i.e. { - // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", - // "idCardPhotoAt": "now()" - // } - newPatch.frontCameraPath = path.join(rpath, hash + '.jpg') - newPatch.frontCameraAt = 'now()' + // update db record patch + // i.e. { + // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", + // "idCardPhotoAt": "now()" + // } + newPatch.frontCameraPath = path.join(rpath, hash + '.jpg') + newPatch.frontCameraAt = 'now()' - // write image file - return writeFile(filename, decodedImageData) - .then(() => newPatch) - }) + // write image file + return writeFile(filename, decodedImageData).then(() => newPatch) + }) } -function addCustomField (customerId, label, value) { +function addCustomField(customerId, label, value) { const sql = `SELECT * FROM custom_field_definitions WHERE label=$1 LIMIT 1` - return db.oneOrNone(sql, [label]) - .then(res => db.tx(t => { - if (_.isNil(res)) { - const fieldId = uuid.v4() - const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label]) - const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value]) - return t.batch([q1, q2]) - } + return db + .oneOrNone(sql, [label]) + .then(res => + db.tx(t => { + if (_.isNil(res)) { + const fieldId = uuid.v4() + const q1 = t.none( + `INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, + [fieldId, label], + ) + const q2 = t.none( + `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, + [customerId, fieldId, value], + ) + return t.batch([q1, q2]) + } - if (!_.isNil(res) && !res.active) { - const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id]) - const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) - return t.batch([q1, q2]) - } else if (!_.isNil(res) && res.active) { - const q1 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) - return t.batch([q1]) - } - }) + if (!_.isNil(res) && !res.active) { + const q1 = t.none( + `UPDATE custom_field_definitions SET active = true WHERE id=$1`, + [res.id], + ) + const q2 = t.none( + `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, + [customerId, res.id, value], + ) + return t.batch([q1, q2]) + } else if (!_.isNil(res) && res.active) { + const q1 = t.none( + `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, + [customerId, res.id, value], + ) + return t.batch([q1]) + } + }), ) .then(res => !_.isNil(res)) } -function saveCustomField (customerId, fieldId, newValue) { +function saveCustomField(customerId, fieldId, newValue) { const sql = `UPDATE customer_custom_field_pairs SET value=$1 WHERE customer_id=$2 AND custom_field_id=$3` return db.none(sql, [newValue, customerId, fieldId]) } -function removeCustomField (customerId, fieldId) { +function removeCustomField(customerId, fieldId) { const sql = `SELECT * FROM customer_custom_field_pairs WHERE custom_field_id=$1` - return db.any(sql, [fieldId]) - .then(res => db.tx(t => { + return db.any(sql, [fieldId]).then(res => + db.tx(t => { // Is the field to be removed the only one of its kind in the pairs table? if (_.size(res) === 1) { - const q1 = t.none(`DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, [customerId, fieldId]) - const q2 = t.none(`UPDATE custom_field_definitions SET active = false WHERE id=$1`, [fieldId]) + const q1 = t.none( + `DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, + [customerId, fieldId], + ) + const q2 = t.none( + `UPDATE custom_field_definitions SET active = false WHERE id=$1`, + [fieldId], + ) return t.batch([q1, q2]) } else { - const q1 = t.none(`DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, [customerId, fieldId]) + const q1 = t.none( + `DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, + [customerId, fieldId], + ) return t.batch([q1]) } - })) + }), + ) } -function getCustomInfoRequestsData (customer) { +function getCustomInfoRequestsData(customer) { if (!customer) return const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1` - return db.any(sql, [customer.id]).then(res => _.set('custom_info_request_data', res, customer)) + return db + .any(sql, [customer.id]) + .then(res => _.set('custom_info_request_data', res, customer)) } -function enableTestCustomer (customerId) { +function enableTestCustomer(customerId) { const sql = `UPDATE customers SET is_test_customer=true WHERE id=$1` return db.none(sql, [customerId]) } -function disableTestCustomer (customerId) { +function disableTestCustomer(customerId) { const sql = `UPDATE customers SET is_test_customer=false WHERE id=$1` return db.none(sql, [customerId]) } -function updateLastAuthAttempt (customerId, deviceId) { +function updateLastAuthAttempt(customerId, deviceId) { const sql = `UPDATE customers SET last_auth_attempt=NOW(), last_used_machine=$2 WHERE id=$1` return db.none(sql, [customerId, deviceId]) } -function getExternalComplianceMachine (customer) { - return settingsLoader.loadLatest() +function getExternalComplianceMachine(customer) { + return settingsLoader + .loadLatest() .then(settings => externalCompliance.getStatusMap(settings, customer.id)) .then(statusMap => { return updateExternalComplianceByMap(customer.id, statusMap) - .then(() => customer.externalCompliance = statusMap) + .then(() => (customer.externalCompliance = statusMap)) .then(() => customer) }) } @@ -963,14 +1034,17 @@ function updateExternalComplianceByMap(customerId, serviceMap) { WHERE customer_id=$2 AND service=$3 ` const pairs = _.toPairs(serviceMap) - const promises = _.map(([service, status]) => db.none(sql, [status.answer, customerId, service]))(pairs) + const promises = _.map(([service, status]) => + db.none(sql, [status.answer, customerId, service]), + )(pairs) return Promise.all(promises) } function getExternalCompliance(customer) { const sql = `SELECT external_id, service, last_known_status, last_updated FROM customer_external_compliance where customer_id=$1` - return db.manyOrNone(sql, [customer.id]) + return db + .manyOrNone(sql, [customer.id]) .then(compliance => { customer.externalCompliance = compliance }) @@ -984,42 +1058,53 @@ function getOpenExternalCompliance() { function notifyRetryExternalCompliance(settings, customerId, service) { const sql = 'SELECT phone FROM customers WHERE id=$1' - const promises = [db.one(sql, [customerId]), externalCompliance.createLink(settings, service, customerId)] + const promises = [ + db.one(sql, [customerId]), + externalCompliance.createLink(settings, service, customerId), + ] - return Promise.all(promises) - .then(([toNumber, link]) => { - const body = `Your external compliance verification has failed. Please try again. Link for retry: ${link}` + return Promise.all(promises).then(([toNumber, link]) => { + const body = `Your external compliance verification has failed. Please try again. Link for retry: ${link}` - return sms.sendMessage(settings, { toNumber, body }) - }) + return sms.sendMessage(settings, { toNumber, body }) + }) } function notifyApprovedExternalCompliance(settings, customerId) { const sql = 'SELECT phone FROM customers WHERE id=$1' - return db.one(sql, [customerId]) - .then((toNumber) => { - const body = 'Your external compliance verification has been approved.' + return db.one(sql, [customerId]).then(toNumber => { + const body = 'Your external compliance verification has been approved.' - return sms.sendMessage(settings, { toNumber, body }) - }) + return sms.sendMessage(settings, { toNumber, body }) + }) } function checkExternalCompliance(settings) { - return getOpenExternalCompliance() - .then(externals => { - console.log(externals) - const promises = _.map(external => { - return externalCompliance.getStatus(settings, external.service, external.customer_id) - .then(status => { - console.log('status', status, external.customer_id, external.service) - if (status.status.answer === RETRY) notifyRetryExternalCompliance(settings, external.customer_id, status.service) - if (status.status.answer === APPROVED) notifyApprovedExternalCompliance(settings, external.customer_id) + return getOpenExternalCompliance().then(externals => { + console.log(externals) + const promises = _.map(external => { + return externalCompliance + .getStatus(settings, external.service, external.customer_id) + .then(status => { + console.log('status', status, external.customer_id, external.service) + if (status.status.answer === RETRY) + notifyRetryExternalCompliance( + settings, + external.customer_id, + status.service, + ) + if (status.status.answer === APPROVED) + notifyApprovedExternalCompliance(settings, external.customer_id) - return updateExternalCompliance(external.customer_id, external.service, status.status.answer) - }) - }, externals) - return Promise.all(promises) - }) + return updateExternalCompliance( + external.customer_id, + external.service, + status.status.answer, + ) + }) + }, externals) + return Promise.all(promises) + }) } function addExternalCompliance(customerId, service, id) { @@ -1027,7 +1112,6 @@ function addExternalCompliance(customerId, service, id) { return db.none(sql, [customerId, id, service]) } - module.exports = { add, addWithEmail, @@ -1054,5 +1138,5 @@ module.exports = { disableTestCustomer, updateLastAuthAttempt, addExternalCompliance, - checkExternalCompliance + checkExternalCompliance, } diff --git a/packages/server/lib/db-migrate-store.js b/packages/server/lib/db-migrate-store.js index 6a0ca9c2..94f09624 100644 --- a/packages/server/lib/db-migrate-store.js +++ b/packages/server/lib/db-migrate-store.js @@ -1,15 +1,15 @@ const db = require('../lib/db') const logger = require('./logger') -const upsert = 'insert into migrations (id, data) values (1, $1) on conflict (id) do update set data = $1' +const upsert = + 'insert into migrations (id, data) values (1, $1) on conflict (id) do update set data = $1' -function DbMigrateStore () { -} +function DbMigrateStore() {} DbMigrateStore.prototype.save = function (set, fn) { let insertData = JSON.stringify({ lastRun: set.lastRun, - migrations: set.migrations + migrations: set.migrations, }) db.none(upsert, [insertData]).then(fn).catch(logger.error) } diff --git a/packages/server/lib/db.js b/packages/server/lib/db.js index e55b4964..fc942f2d 100644 --- a/packages/server/lib/db.js +++ b/packages/server/lib/db.js @@ -16,9 +16,8 @@ const pgp = Pgp({ else if (e.query) { logger.error(e.query) e.params && logger.error(e.params) - } - else logger.error(err) - } + } else logger.error(err) + }, }) const db = pgp(PSQL_URL) diff --git a/packages/server/lib/email.js b/packages/server/lib/email.js index 53367ca4..b0abb024 100644 --- a/packages/server/lib/email.js +++ b/packages/server/lib/email.js @@ -1,25 +1,25 @@ const ph = require('./plugin-helper') -function sendMessage (settings, rec) { - return Promise.resolve() - .then(() => { - const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun' - const plugin = ph.load(ph.EMAIL, pluginCode) - const account = settings.accounts[pluginCode] +function sendMessage(settings, rec) { + return Promise.resolve().then(() => { + const pluginCode = + settings.config.notifications_thirdParty_email || 'mailgun' + const plugin = ph.load(ph.EMAIL, pluginCode) + const account = settings.accounts[pluginCode] - return plugin.sendMessage(account, rec) - }) + return plugin.sendMessage(account, rec) + }) } -function sendCustomerMessage (settings, rec) { - return Promise.resolve() - .then(() => { - const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun' - const plugin = ph.load(ph.EMAIL, pluginCode) - const account = settings.accounts[pluginCode] +function sendCustomerMessage(settings, rec) { + return Promise.resolve().then(() => { + const pluginCode = + settings.config.notifications_thirdParty_email || 'mailgun' + const plugin = ph.load(ph.EMAIL, pluginCode) + const account = settings.accounts[pluginCode] - return plugin.sendMessage(account, rec) - }) + return plugin.sendMessage(account, rec) + }) } -module.exports = {sendMessage, sendCustomerMessage} +module.exports = { sendMessage, sendCustomerMessage } diff --git a/packages/server/lib/environment-helper.js b/packages/server/lib/environment-helper.js index 777dcef4..04d900a0 100644 --- a/packages/server/lib/environment-helper.js +++ b/packages/server/lib/environment-helper.js @@ -5,11 +5,11 @@ const isProdMode = () => process.env.NODE_ENV === 'production' require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) -function isRemoteNode (crypto) { +function isRemoteNode(crypto) { return process.env[`${crypto.cryptoCode}_NODE_LOCATION`] === 'remote' } -function isRemoteWallet (crypto) { +function isRemoteWallet(crypto) { return process.env[`${crypto.cryptoCode}_WALLET_LOCATION`] === 'remote' } @@ -17,5 +17,5 @@ module.exports = { isDevMode, isProdMode, isRemoteNode, - isRemoteWallet + isRemoteWallet, } diff --git a/packages/server/lib/error.js b/packages/server/lib/error.js index 87be70cc..d9a5188c 100644 --- a/packages/server/lib/error.js +++ b/packages/server/lib/error.js @@ -15,7 +15,7 @@ const E = function (name) { module.exports = E -function register (errorName) { +function register(errorName) { E[errorName] = E(errorName) } diff --git a/packages/server/lib/event-bus.js b/packages/server/lib/event-bus.js index 0c481327..74dc7367 100644 --- a/packages/server/lib/event-bus.js +++ b/packages/server/lib/event-bus.js @@ -5,7 +5,7 @@ const _ = require('lodash/fp') const subscriptions = {} -function subscribe (eventType, callback) { +function subscribe(eventType, callback) { const id = uuid.v1() if (!subscriptions[eventType]) subscriptions[eventType] = {} @@ -15,15 +15,18 @@ function subscribe (eventType, callback) { return { unsubscribe: () => { delete subscriptions[eventType][id] - if (_.keys(subscriptions[eventType]).length === 0) delete subscriptions[eventType] - } + if (_.keys(subscriptions[eventType]).length === 0) + delete subscriptions[eventType] + }, } } -function publish (eventType, arg) { +function publish(eventType, arg) { if (!subscriptions[eventType]) return - _.keys(subscriptions[eventType]).forEach(key => subscriptions[eventType][key](arg)) + _.keys(subscriptions[eventType]).forEach(key => + subscriptions[eventType][key](arg), + ) } module.exports = { subscribe, publish } diff --git a/packages/server/lib/exchange.js b/packages/server/lib/exchange.js index 00316eae..2c5b1c24 100644 --- a/packages/server/lib/exchange.js +++ b/packages/server/lib/exchange.js @@ -6,71 +6,80 @@ const ccxt = require('./plugins/exchange/ccxt') const mockExchange = require('./plugins/exchange/mock-exchange') const accounts = require('./new-admin/config/accounts') -function lookupExchange (settings, cryptoCode) { - const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange +function lookupExchange(settings, cryptoCode) { + const exchange = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).exchange if (exchange === 'no-exchange') return null return exchange } -function fetchExchange (settings, cryptoCode) { - return Promise.resolve() - .then(() => { - const exchangeName = lookupExchange(settings, cryptoCode) - if (exchangeName === 'mock-exchange') return { exchangeName, account: { currencyMarket: 'EUR' } } - if (!exchangeName) throw new Error('No exchange set') - const account = settings.accounts[exchangeName] +function fetchExchange(settings, cryptoCode) { + return Promise.resolve().then(() => { + const exchangeName = lookupExchange(settings, cryptoCode) + if (exchangeName === 'mock-exchange') + return { exchangeName, account: { currencyMarket: 'EUR' } } + if (!exchangeName) throw new Error('No exchange set') + const account = settings.accounts[exchangeName] - return { exchangeName, account } - }) + return { exchangeName, account } + }) } -function buy (settings, tradeEntry) { +function buy(settings, tradeEntry) { const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry - return fetchExchange(settings, cryptoCode) - .then(r => { - if (r.exchangeName === 'mock-exchange') { - return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode) - } - return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName) - }) + return fetchExchange(settings, cryptoCode).then(r => { + if (r.exchangeName === 'mock-exchange') { + return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode) + } + return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName) + }) } -function sell (settings, tradeEntry) { +function sell(settings, tradeEntry) { const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry - return fetchExchange(settings, cryptoCode) - .then(r => { - if (r.exchangeName === 'mock-exchange') { - return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode) - } - return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName) - }) + return fetchExchange(settings, cryptoCode).then(r => { + if (r.exchangeName === 'mock-exchange') { + return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode) + } + return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName) + }) } -function active (settings, cryptoCode) { +function active(settings, cryptoCode) { return !!lookupExchange(settings, cryptoCode) } -function getMarkets () { - const filterExchanges = _.filter(it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange') - const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST)) +function getMarkets() { + const filterExchanges = _.filter( + it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange', + ) + const availableExchanges = _.map( + it => it.code, + filterExchanges(accounts.ACCOUNT_LIST), + ) const fetchMarketForExchange = exchange => - ccxt.getMarkets(exchange, ALL_CRYPTOS) + ccxt + .getMarkets(exchange, ALL_CRYPTOS) .then(markets => ({ exchange, markets })) .catch(error => ({ exchange, markets: [], - error: error.message + error: error.message, })) - const transformToObject = _.reduce((acc, { exchange, markets }) => ({ - ...acc, - [exchange]: markets - }), {}) + const transformToObject = _.reduce( + (acc, { exchange, markets }) => ({ + ...acc, + [exchange]: markets, + }), + {}, + ) const promises = _.map(fetchMarketForExchange, availableExchanges) - return Promise.all(promises) - .then(transformToObject) + return Promise.all(promises).then(transformToObject) } module.exports = { @@ -78,5 +87,5 @@ module.exports = { buy, sell, active, - getMarkets + getMarkets, } diff --git a/packages/server/lib/forex.js b/packages/server/lib/forex.js index 6c5ea781..dc71bef7 100644 --- a/packages/server/lib/forex.js +++ b/packages/server/lib/forex.js @@ -7,34 +7,52 @@ const T = require('./time') const MAX_ROTATIONS = 5 -const _getFiatRates = () => ( - axios.get('https://bitpay.com/api/rates') - .then(response => response.data) -) +const _getFiatRates = () => + axios.get('https://bitpay.com/api/rates').then(response => response.data) const getFiatRates = mem(_getFiatRates, { maxAge: 6 * T.hours, - cacheKey: () => '' + cacheKey: () => '', }) const API_QUEUE = [ - { api: getBitPayFxRate, name: 'bitpay', fiatCodeProperty: 'code', rateProperty: 'rate' } + { + api: getBitPayFxRate, + name: 'bitpay', + fiatCodeProperty: 'code', + rateProperty: 'rate', + }, ] -function getBitPayFxRate (fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty) { - return getFiatRates() - .then(({ data: fxRates }) => { - const defaultFiatRate = findCurrencyRates(fxRates, defaultFiatMarket, fiatCodeProperty, rateProperty) - const fxRate = findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty).div(defaultFiatRate) - return { - fxRate - } - }) +function getBitPayFxRate( + fiatCode, + defaultFiatMarket, + fiatCodeProperty, + rateProperty, +) { + return getFiatRates().then(({ data: fxRates }) => { + const defaultFiatRate = findCurrencyRates( + fxRates, + defaultFiatMarket, + fiatCodeProperty, + rateProperty, + ) + const fxRate = findCurrencyRates( + fxRates, + fiatCode, + fiatCodeProperty, + rateProperty, + ).div(defaultFiatRate) + return { + fxRate, + } + }) } -function findCurrencyRates (fxRates, fiatCode, fiatCodeProperty, rateProperty) { +function findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty) { const rates = _.find(_.matchesProperty(fiatCodeProperty, fiatCode), fxRates) - if (!rates || !rates[rateProperty]) throw new Error(`Unsupported currency: ${fiatCode}`) + if (!rates || !rates[rateProperty]) + throw new Error(`Unsupported currency: ${fiatCode}`) return new BN(rates[rateProperty].toString()) } @@ -46,15 +64,20 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => { if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`) - return activeAPI(fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty) - .catch(() => { - // Switch service - const erroredService = API_QUEUE.shift() - API_QUEUE.push(erroredService) - if (retries >= MAX_ROTATIONS) throw new Error(`FOREX API error from ${erroredService.name}`) + return activeAPI( + fiatCode, + defaultFiatMarket, + fiatCodeProperty, + rateProperty, + ).catch(() => { + // Switch service + const erroredService = API_QUEUE.shift() + API_QUEUE.push(erroredService) + if (retries >= MAX_ROTATIONS) + throw new Error(`FOREX API error from ${erroredService.name}`) - return getRate(++retries, fiatCode) - }) + return getRate(++retries, fiatCode) + }) } module.exports = { getFiatRates, getRate } diff --git a/packages/server/lib/graphql/resolvers.js b/packages/server/lib/graphql/resolvers.js index 2654c058..433273b0 100644 --- a/packages/server/lib/graphql/resolvers.js +++ b/packages/server/lib/graphql/resolvers.js @@ -4,7 +4,10 @@ const nmd = require('nano-markdown') const plugins = require('../plugins') const configManager = require('../new-config-manager') const settingsLoader = require('../new-settings-loader') -const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests') +const { + batchGetCustomInfoRequest, + getCustomInfoRequests, +} = require('../new-admin/services/customInfoRequests') const state = require('../middlewares/state') const { getMachine } = require('../machine-loader') @@ -14,23 +17,26 @@ const urlsToPing = [ `us.archive.ubuntu.com`, `uk.archive.ubuntu.com`, `za.archive.ubuntu.com`, - `cn.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 - } + size: 44668, + }, ] const addSmthInfo = (dstField, srcFields) => smth => - (smth && smth.active) ? _.set(dstField, _.pick(srcFields, smth)) : _.identity + smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity -const addOperatorInfo = addSmthInfo( - 'operatorInfo', - ['name', 'phone', 'email', 'website', 'companyNumber'] -) +const addOperatorInfo = addSmthInfo('operatorInfo', [ + 'name', + 'phone', + 'email', + 'website', + 'companyNumber', +]) const addReceiptInfo = receiptInfo => ret => { if (!receiptInfo) return ret @@ -56,87 +62,90 @@ const addReceiptInfo = receiptInfo => ret => { _.pick(fields), )(receiptInfo) - return (receiptInfo.paper || receiptInfo.sms) ? - _.set('receiptInfo', receiptInfo, ret) : - ret + return receiptInfo.paper || receiptInfo.sms + ? _.set('receiptInfo', receiptInfo, ret) + : ret } - -const addMachineScreenOpts = smth => _.update( - 'screenOptions', - _.flow( - addSmthInfo( - 'rates', - [ - 'active' - ] - )(smth.rates) +const addMachineScreenOpts = smth => + _.update( + 'screenOptions', + _.flow(addSmthInfo('rates', ['active'])(smth.rates)), ) -) /* TODO: Simplify this. */ const buildTriggers = allTriggers => { const normalTriggers = [] const customTriggers = _.filter(o => { - if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o) + if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) + normalTriggers.push(o) return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId) }, allTriggers) return _.flow( _.map(_.get('customInfoRequestId')), - batchGetCustomInfoRequest - )(customTriggers) - .then(res => { - res.forEach((details, index) => { - // make sure we aren't attaching the details to the wrong trigger - if (customTriggers[index].customInfoRequestId !== details.id) return - customTriggers[index] = { ...customTriggers[index], customInfoRequest: details } - }) - return [...normalTriggers, ...customTriggers] + batchGetCustomInfoRequest, + )(customTriggers).then(res => { + res.forEach((details, index) => { + // make sure we aren't attaching the details to the wrong trigger + if (customTriggers[index].customInfoRequestId !== details.id) return + customTriggers[index] = { + ...customTriggers[index], + customInfoRequest: details, + } }) + return [...normalTriggers, ...customTriggers] + }) } -const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => { - const massageCoins = _.map(_.pick([ - 'batchable', - 'cashInCommission', - 'cashInFee', - 'cashOutCommission', - 'cashOutFee', - 'cryptoCode', - 'cryptoCodeDisplay', - 'cryptoNetwork', - 'cryptoUnits', - 'display', - 'minimumTx', - 'isCashInOnly' - ])) +const staticConfig = ({ + currentConfigVersion, + deviceId, + deviceName, + pq, + settings, +}) => { + const massageCoins = _.map( + _.pick([ + 'batchable', + 'cashInCommission', + 'cashInFee', + 'cashOutCommission', + 'cashOutFee', + 'cryptoCode', + 'cryptoCodeDisplay', + 'cryptoNetwork', + 'cryptoUnits', + 'display', + 'minimumTx', + 'isCashInOnly', + ]), + ) const staticConf = _.flow( - _.pick([ - 'coins', - 'configVersion', - 'timezone', - 'screenOptions' - ]), + _.pick(['coins', 'configVersion', 'timezone', 'screenOptions']), _.update('coins', massageCoins), _.set('serverVersion', VERSION), )(pq) return Promise.all([ !!configManager.getCompliance(settings.config).enablePaperWalletOnly, - configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config), + configManager.getTriggersAutomation( + getCustomInfoRequests(true), + settings.config, + ), buildTriggers(configManager.getTriggers(settings.config)), - configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2', + configManager.getWalletSettings('BTC', settings.config).layer2 !== + 'no-layer2', configManager.getLocale(deviceId, settings.config), configManager.getOperatorInfo(settings.config), configManager.getReceipt(settings.config), configManager.getAllMachineScreenOpts(settings.config), !!configManager.getCashOut(deviceId, settings.config).active, getMachine(deviceId, currentConfigVersion), - configManager.getCustomerAuthenticationMethod(settings.config) - ]) - .then(([ + configManager.getCustomerAuthenticationMethod(settings.config), + ]).then( + ([ enablePaperWalletOnly, triggersAutomation, triggers, @@ -149,88 +158,121 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings { numberOfCassettes, numberOfRecyclers }, customerAuthentication, ]) => - (currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ? - null : - _.flow( - _.assign({ - enablePaperWalletOnly, - triggersAutomation, - triggers, - hasLightning, - localeInfo: { - country: localeInfo.country, - languages: localeInfo.languages, - fiatCode: localeInfo.fiatCurrency - }, - machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers }, - twoWayMode, - customerAuthentication, - speedtestFiles, - urlsToPing, - }), - addOperatorInfo(operatorInfo), - addReceiptInfo(receiptInfo), - addMachineScreenOpts(machineScreenOpts) - )(staticConf)) + currentConfigVersion && currentConfigVersion >= staticConf.configVersion + ? null + : _.flow( + _.assign({ + enablePaperWalletOnly, + triggersAutomation, + triggers, + hasLightning, + localeInfo: { + country: localeInfo.country, + languages: localeInfo.languages, + fiatCode: localeInfo.fiatCurrency, + }, + machineInfo: { + deviceId, + deviceName, + numberOfCassettes, + numberOfRecyclers, + }, + twoWayMode, + customerAuthentication, + speedtestFiles, + urlsToPing, + }), + addOperatorInfo(operatorInfo), + addReceiptInfo(receiptInfo), + addMachineScreenOpts(machineScreenOpts), + )(staticConf), + ) } - const setZeroConfLimit = config => coin => _.set( 'zeroConfLimit', configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit ?? 0, - coin + coin, ) -const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => { +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 + cassettes + ? _.flow( + cassettes => + _.set('physical', _.get('cassettes', cassettes), cassettes), + cassettes => + _.set('virtual', _.get('virtualCassettes', cassettes), cassettes), + _.unset('cassettes'), + _.unset('virtualCassettes'), + )(cassettes) + : null const massageRecyclers = recyclers => - recyclers ? - _.flow( - recyclers => _.set('physical', _.get('recyclers', recyclers), recyclers), - recyclers => _.set('virtual', _.get('virtualRecyclers', recyclers), recyclers), - _.unset('recyclers'), - _.unset('virtualRecyclers') - )(recyclers) : - null + recyclers + ? _.flow( + recyclers => + _.set('physical', _.get('recyclers', recyclers), recyclers), + recyclers => + _.set('virtual', _.get('virtualRecyclers', recyclers), recyclers), + _.unset('recyclers'), + _.unset('virtualRecyclers'), + )(recyclers) + : null - state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids) + state.pids = _.update( + operatorId, + _.set(deviceId, { pid, ts: Date.now() }), + state.pids, + ) const res = _.flow( - _.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'recyclers', 'coins', 'rates']), + _.pick([ + 'areThereAvailablePromoCodes', + 'balances', + 'cassettes', + 'recyclers', + 'coins', + 'rates', + ]), _.update('cassettes', massageCassettes), _.update('recyclers', massageRecyclers), /* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */ - _.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])), + _.update( + 'coins', + _.map(({ cryptoCode, rates }) => [cryptoCode, rates]), + ), /* [{ cryptoCode: balance }, ...] => [[cryptoCode, { balance }], ...] */ - _.update('balances', _.flow( - _.toPairs, - _.map(([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 }, ... } */ - ({ areThereAvailablePromoCodes, balances, cassettes, recyclers, coins, rates }) => ({ + ({ + areThereAvailablePromoCodes, + balances, + cassettes, + recyclers, + coins, + rates, + }) => ({ areThereAvailablePromoCodes, cassettes, recyclers, coins: _.flow( _.reduce( (ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret), - rates + rates, ), /* { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } => [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] */ @@ -240,17 +282,36 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => { _.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)), /* Only send coins which have all information needed by the machine. This prevents the machine going down if there's an issue with the coin node */ - _.filter(coin => ['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(it => it in coin)) - )(_.concat(balances, coins)) + _.filter(coin => + ['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every( + it => it in coin, + ), + ), + )(_.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), - _.set('emptyUnit', !!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid), - _.set('refillUnit', !!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid), - _.set('diagnostics', !!pid && state.diagnostics?.[operatorId]?.[deviceId] === pid), + _.set( + 'shutdown', + !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid, + ), + _.set( + 'restartServices', + !!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid, + ), + _.set( + 'emptyUnit', + !!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid, + ), + _.set( + 'refillUnit', + !!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid, + ), + _.set( + 'diagnostics', + !!pid && state.diagnostics?.[operatorId]?.[deviceId] === pid, + ), )(pq) // Clean up the state middleware and prevent commands from being issued more than once @@ -269,8 +330,11 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => { return res } - -const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) => +const configs = ( + parent, + { currentConfigVersion }, + { deviceId, deviceName, operatorId, pid, settings }, +) => plugins(settings, deviceId) .pollQueries() .then(pq => ({ @@ -290,15 +354,17 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera }), })) - -const massageTerms = terms => (terms.active && terms.text) ? ({ - tcPhoto: Boolean(terms.tcPhoto), - delay: Boolean(terms.delay), - title: terms.title, - text: nmd(terms.text), - accept: terms.acceptButtonText, - cancel: terms.cancelButtonText, -}) : null +const massageTerms = terms => + terms.active && terms.text + ? { + tcPhoto: Boolean(terms.tcPhoto), + 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 @@ -327,7 +393,7 @@ const massageTerms = terms => (terms.active && terms.text) ? ({ * If the `hash` differs from `currentHash` then everything is resent (to * simplify machine implementation). */ -const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settings }, info) => { +const terms = (parent, { currentConfigVersion, currentHash }, { settings }) => { const isNone = x => _.isNil(x) || _.isEmpty(x) let latestTerms = configManager.getTermsConditions(settings.config) @@ -342,17 +408,22 @@ const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settin const isHashNew = hash !== currentHash const text = isHashNew ? latestTerms.text : null - return settingsLoader.fetchCurrentConfigVersion() + return settingsLoader + .fetchCurrentConfigVersion() .catch(() => null) - .then(configVersion => isHashNew || _.isNil(currentConfigVersion) || currentConfigVersion < configVersion) - .then(isVersionNew => isVersionNew ? _.omit(['text'], latestTerms) : null) + .then( + configVersion => + isHashNew || + _.isNil(currentConfigVersion) || + currentConfigVersion < configVersion, + ) + .then(isVersionNew => (isVersionNew ? _.omit(['text'], latestTerms) : null)) .then(details => ({ hash, details, text })) } - module.exports = { Query: { configs, terms, - } + }, } diff --git a/packages/server/lib/graphql/server.js b/packages/server/lib/graphql/server.js index 7d6c8be8..2d8cd5ac 100644 --- a/packages/server/lib/graphql/server.js +++ b/packages/server/lib/graphql/server.js @@ -5,11 +5,11 @@ const { ApolloServer } = require('@apollo/server') const devMode = !!require('minimist')(process.argv.slice(2)).dev const 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 */ + 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 */ + settings: req.settings /* lib/middlewares/populateSettings.js */, }) const graphQLServer = new ApolloServer({ @@ -21,7 +21,7 @@ const graphQLServer = new ApolloServer({ return error }, includeStacktraceInErrorResponses: devMode, - logger + logger, }) -module.exports = { graphQLServer, context } \ No newline at end of file +module.exports = { graphQLServer, context } diff --git a/packages/server/lib/graphql/types.js b/packages/server/lib/graphql/types.js index a4dfa4c9..cca000b4 100644 --- a/packages/server/lib/graphql/types.js +++ b/packages/server/lib/graphql/types.js @@ -1,234 +1,234 @@ const gql = require('graphql-tag') module.exports = gql` -type Coin { - cryptoCode: String! - cryptoCodeDisplay: String! - display: String! - minimumTx: String! - cashInFee: String! - cashOutFee: String! - cashInCommission: String! - cashOutCommission: String! - cryptoNetwork: String! - cryptoUnits: String! - batchable: Boolean! - isCashInOnly: Boolean! -} + type Coin { + cryptoCode: String! + cryptoCodeDisplay: String! + display: String! + minimumTx: String! + cashInFee: String! + cashOutFee: String! + cashInCommission: String! + cashOutCommission: String! + cryptoNetwork: String! + cryptoUnits: String! + batchable: Boolean! + isCashInOnly: Boolean! + } -type LocaleInfo { - country: String! - fiatCode: String! - languages: [String!]! -} + type LocaleInfo { + country: String! + fiatCode: String! + languages: [String!]! + } -type OperatorInfo { - name: String! - phone: String! - email: String! - website: String! - companyNumber: String! -} + type OperatorInfo { + name: String! + phone: String! + email: String! + website: String! + companyNumber: String! + } -type MachineInfo { - deviceId: String! @deprecated(reason: "unused by the machine") - deviceName: String - numberOfCassettes: Int - numberOfRecyclers: Int -} + type MachineInfo { + deviceId: String! @deprecated(reason: "unused by the machine") + deviceName: String + numberOfCassettes: Int + numberOfRecyclers: Int + } -type ReceiptInfo { - paper: Boolean! - automaticPrint: Boolean! - sms: Boolean! - operatorWebsite: Boolean! - operatorEmail: Boolean! - operatorPhone: Boolean! - companyNumber: Boolean! - machineLocation: Boolean! - customerNameOrPhoneNumber: Boolean! - exchangeRate: Boolean! - addressQRCode: Boolean! -} + type ReceiptInfo { + paper: Boolean! + automaticPrint: Boolean! + sms: Boolean! + operatorWebsite: Boolean! + operatorEmail: Boolean! + operatorPhone: Boolean! + companyNumber: Boolean! + machineLocation: Boolean! + customerNameOrPhoneNumber: Boolean! + exchangeRate: Boolean! + addressQRCode: Boolean! + } -type MachineScreenOptions { - rates: RateScreenOptions! -} + type MachineScreenOptions { + rates: RateScreenOptions! + } -type RateScreenOptions { - active: Boolean! -} + type RateScreenOptions { + active: Boolean! + } -type SpeedtestFile { - url: String! - size: Int! -} + type SpeedtestFile { + url: String! + size: Int! + } -enum TriggerAutomationType { - Automatic - Manual -} + enum TriggerAutomationType { + Automatic + Manual + } -type CustomTriggersAutomation { - id: ID! - type: TriggerAutomationType! -} + type CustomTriggersAutomation { + id: ID! + type: TriggerAutomationType! + } -type TriggersAutomation { - sanctions: TriggerAutomationType! - idCardPhoto: TriggerAutomationType! - idCardData: TriggerAutomationType! - facephoto: TriggerAutomationType! - usSsn: TriggerAutomationType! - custom: [CustomTriggersAutomation]! -} + type TriggersAutomation { + sanctions: TriggerAutomationType! + idCardPhoto: TriggerAutomationType! + idCardData: TriggerAutomationType! + facephoto: TriggerAutomationType! + usSsn: TriggerAutomationType! + custom: [CustomTriggersAutomation]! + } -type CustomScreen { - text: String! - title: String! -} + type CustomScreen { + text: String! + title: String! + } -type CustomInput { - type: String! - constraintType: String! - label1: String - label2: String - choiceList: [String] -} + type CustomInput { + type: String! + constraintType: String! + label1: String + label2: String + choiceList: [String] + } -type CustomRequest { - name: String! - input: CustomInput! - screen1: CustomScreen! - screen2: CustomScreen! -} + type CustomRequest { + name: String! + input: CustomInput! + screen1: CustomScreen! + screen2: CustomScreen! + } -type CustomInfoRequest { - id: String! - enabled: Boolean! - customRequest: CustomRequest! -} + type CustomInfoRequest { + id: String! + enabled: Boolean! + customRequest: CustomRequest! + } -type Trigger { - id: String! - direction: String! - requirement: String! - triggerType: String! + type Trigger { + id: String! + direction: String! + requirement: String! + triggerType: String! - suspensionDays: Float - threshold: Int - thresholdDays: Int - customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id") - customInfoRequest: CustomInfoRequest - externalService: String -} + suspensionDays: Float + threshold: Int + thresholdDays: Int + customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id") + customInfoRequest: CustomInfoRequest + externalService: String + } -type TermsDetails { - tcPhoto: Boolean! - delay: Boolean! - title: String! - accept: String! - cancel: String! -} + type TermsDetails { + tcPhoto: Boolean! + delay: Boolean! + title: String! + accept: String! + cancel: String! + } -type Terms { - hash: String! - text: String - details: TermsDetails -} + type Terms { + hash: String! + text: String + details: TermsDetails + } -enum CustomerAuthentication { - EMAIL - SMS -} + enum CustomerAuthentication { + EMAIL + SMS + } -type StaticConfig { - configVersion: Int! + type StaticConfig { + configVersion: Int! - coins: [Coin!]! - enablePaperWalletOnly: Boolean! - hasLightning: Boolean! - serverVersion: String! - timezone: Int! - twoWayMode: Boolean! - customerAuthentication: CustomerAuthentication! + coins: [Coin!]! + enablePaperWalletOnly: Boolean! + hasLightning: Boolean! + serverVersion: String! + timezone: Int! + twoWayMode: Boolean! + customerAuthentication: CustomerAuthentication! - localeInfo: LocaleInfo! - operatorInfo: OperatorInfo - machineInfo: MachineInfo! - receiptInfo: ReceiptInfo - screenOptions: MachineScreenOptions + localeInfo: LocaleInfo! + operatorInfo: OperatorInfo + machineInfo: MachineInfo! + receiptInfo: ReceiptInfo + screenOptions: MachineScreenOptions - speedtestFiles: [SpeedtestFile!]! - urlsToPing: [String!]! + speedtestFiles: [SpeedtestFile!]! + urlsToPing: [String!]! - triggersAutomation: TriggersAutomation! - triggers: [Trigger!]! -} + 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! + 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! + cryptoCode: String! + balance: String! - # Raw rates - ask: String! - bid: String! + # Raw rates + ask: String! + bid: String! - # Rates with commissions applied - cashIn: String! - cashOut: String! + # Rates with commissions applied + cashIn: String! + cashOut: String! - zeroConfLimit: Int! -} + zeroConfLimit: Int! + } -type PhysicalCassette { - name: String! - denomination: Int! - count: Int! -} + type PhysicalCassette { + name: String! + denomination: Int! + count: Int! + } -type PhysicalRecycler { - name: String! - number: Int! - denomination: Int! - count: Int! -} + type PhysicalRecycler { + name: String! + number: Int! + denomination: Int! + count: Int! + } -type Cassettes { - physical: [PhysicalCassette!]! - virtual: [Int!]! -} + type Cassettes { + physical: [PhysicalCassette!]! + virtual: [Int!]! + } -type Recyclers { - physical: [PhysicalRecycler!]! - virtual: [Int!]! -} + type Recyclers { + physical: [PhysicalRecycler!]! + virtual: [Int!]! + } -type DynamicConfig { - areThereAvailablePromoCodes: Boolean! - cassettes: Cassettes - recyclers: Recyclers - coins: [DynamicCoinValues!]! - reboot: Boolean! - shutdown: Boolean! - restartServices: Boolean! - emptyUnit: Boolean! - refillUnit: Boolean! - diagnostics: Boolean! -} + type DynamicConfig { + areThereAvailablePromoCodes: Boolean! + cassettes: Cassettes + recyclers: Recyclers + coins: [DynamicCoinValues!]! + reboot: Boolean! + shutdown: Boolean! + restartServices: Boolean! + emptyUnit: Boolean! + refillUnit: Boolean! + diagnostics: Boolean! + } -type Configs { - static: StaticConfig - dynamic: DynamicConfig! -} + type Configs { + static: StaticConfig + dynamic: DynamicConfig! + } -type Query { - configs(currentConfigVersion: Int): Configs! - terms(currentHash: String, currentConfigVersion: Int): Terms -} + type Query { + configs(currentConfigVersion: Int): Configs! + terms(currentHash: String, currentConfigVersion: Int): Terms + } ` diff --git a/packages/server/lib/hardware-credentials.js b/packages/server/lib/hardware-credentials.js index 761fd045..d83f8751 100644 --- a/packages/server/lib/hardware-credentials.js +++ b/packages/server/lib/hardware-credentials.js @@ -2,27 +2,27 @@ const uuid = require('uuid') const db = require('./db') -function createHardwareCredential (userID, credentialData) { +function createHardwareCredential(userID, credentialData) { const sql = `INSERT INTO hardware_credentials (id, user_id, data) VALUES ($1, $2, $3)` return db.none(sql, [uuid.v4(), userID, credentialData]) } -function getHardwareCredentials () { +function getHardwareCredentials() { const sql = `SELECT * FROM hardware_credentials` return db.any(sql) } -function getHardwareCredentialsByUserId (userID) { +function getHardwareCredentialsByUserId(userID) { const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1` return db.any(sql, [userID]) } -function getUserByUserHandle (userHandle) { +function getUserByUserHandle(userHandle) { const sql = `SELECT users.id, users.username, users.role FROM users INNER JOIN hardware_credentials hc ON users.id=hc.user_id WHERE data->>'userHandle'=$1::jsonb::text` return db.oneOrNone(sql, [userHandle]) } -function updateHardwareCredential (credential) { +function updateHardwareCredential(credential) { const sql = `UPDATE hardware_credentials SET last_used=now(), data=$1 WHERE id=$2` return db.none(sql, [credential.data, credential.id]) } @@ -32,5 +32,5 @@ module.exports = { getHardwareCredentials, getHardwareCredentialsByUserId, getUserByUserHandle, - updateHardwareCredential + updateHardwareCredential, } diff --git a/packages/server/lib/layer2.js b/packages/server/lib/layer2.js index b77403da..e64289e2 100644 --- a/packages/server/lib/layer2.js +++ b/packages/server/lib/layer2.js @@ -2,38 +2,47 @@ const configManager = require('./new-config-manager') const ph = require('./plugin-helper') const _ = require('lodash/fp') -function fetch (settings, cryptoCode) { - const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2 +function fetch(settings, cryptoCode) { + const plugin = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).layer2 if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve() const layer2 = ph.load(ph.LAYER2, plugin) const account = settings.accounts[plugin] - return Promise.resolve({layer2, account}) + return Promise.resolve({ layer2, account }) } -function newAddress (settings, info) { - return fetch(settings, info.cryptoCode) - .then(r => { - if (!r) return - return r.layer2.newAddress(r.account, info) - }) +function newAddress(settings, info) { + return fetch(settings, info.cryptoCode).then(r => { + if (!r) return + return r.layer2.newAddress(r.account, info) + }) } -function getStatus (settings, tx) { +function getStatus(settings, tx) { const toAddress = tx.layer2Address - if (!toAddress) return Promise.resolve({status: 'notSeen'}) + if (!toAddress) return Promise.resolve({ status: 'notSeen' }) - return fetch(settings, tx.cryptoCode) - .then(r => { - if (!r) return {status: 'notSeen'} - return r.layer2.getStatus(r.account, toAddress, tx.cryptoAtoms, tx.cryptoCode) - }) + return fetch(settings, tx.cryptoCode).then(r => { + if (!r) return { status: 'notSeen' } + return r.layer2.getStatus( + r.account, + toAddress, + tx.cryptoAtoms, + tx.cryptoCode, + ) + }) } -function cryptoNetwork (settings, cryptoCode) { - const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2 +function cryptoNetwork(settings, cryptoCode) { + const plugin = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).layer2 const layer2 = ph.load(ph.LAYER2, plugin) const account = settings.accounts[plugin] @@ -41,7 +50,7 @@ function cryptoNetwork (settings, cryptoCode) { return layer2.cryptoNetwork(account, cryptoCode) } -function isLayer2Address (address) { +function isLayer2Address(address) { return address.split(':').length >= 2 } @@ -49,5 +58,5 @@ module.exports = { isLayer2Address, newAddress, getStatus, - cryptoNetwork + cryptoNetwork, } diff --git a/packages/server/lib/logger.js b/packages/server/lib/logger.js index ef2bda41..3507a706 100644 --- a/packages/server/lib/logger.js +++ b/packages/server/lib/logger.js @@ -7,40 +7,42 @@ const LOG_LEVEL = process.env.LOG_LEVEL const logger = new winston.Logger({ level: LOG_LEVEL, transports: [ - new (winston.transports.Console)({ + new winston.transports.Console({ timestamp: true, colorize: true, handleExceptions: true, - humanReadableUnhandledException: true + humanReadableUnhandledException: true, }), new Postgres({ connectionString: PSQL_URL, tableName: 'server_logs', handleExceptions: true, - humanReadableUnhandledException: true - }) + humanReadableUnhandledException: true, + }), ], rewriters: [ - (...[,, meta]) => { + (...[, , meta]) => { if (meta.isAxiosError) { return { message: meta.message, status: meta.response?.status, data: meta.response?.data, url: meta.config?.url, - method: meta.config?.method + method: meta.config?.method, } } - return meta instanceof Error ? { message: meta.message, stack: meta.stack, meta } : meta - } + return meta instanceof Error + ? { message: meta.message, stack: meta.stack, meta } + : meta + }, ], - exitOnError: false + exitOnError: false, }) logger.stream = { write: message => { logger.info(message.trim()) - } + }, } module.exports = logger diff --git a/packages/server/lib/logs.js b/packages/server/lib/logs.js index 8187e23e..ae8970dd 100644 --- a/packages/server/lib/logs.js +++ b/packages/server/lib/logs.js @@ -7,7 +7,6 @@ const logger = require('./logger') const pgp = require('pg-promise')() const getMachineName = require('./machine-loader').getMachineName -const NUM_RESULTS = 500 /** * Get the latest log's timestamp @@ -20,12 +19,15 @@ const NUM_RESULTS = 500 * * @returns {date} Last timestamp */ -function getLastSeen (deviceId) { +function getLastSeen(deviceId) { const sql = `select id, timestamp, serial from logs where device_id=$1 order by timestamp desc, serial desc limit 1` - return db.oneOrNone(sql, [deviceId]) - .then(log => log ? {timestamp: log.timestamp, serial: log.serial, id: log.id} : null) + return db + .oneOrNone(sql, [deviceId]) + .then(log => + log ? { timestamp: log.timestamp, serial: log.serial, id: log.id } : null, + ) } /** @@ -40,10 +42,11 @@ function getLastSeen (deviceId) { * * @returns {null} */ -function update (deviceId, logLines) { - const cs = new pgp.helpers.ColumnSet([ - 'id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'], - {table: 'logs'}) +function update(deviceId, logLines) { + const cs = new pgp.helpers.ColumnSet( + ['id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'], + { table: 'logs' }, + ) const logs = _.map(log => { const formatted = { @@ -52,7 +55,7 @@ function update (deviceId, logLines) { message: log.msg, logLevel: _.contains('error', _.lowerCase(log.msg)) ? 'error' : 'info', timestamp: log.timestamp, - serial: log.serial || 0 + serial: log.serial || 0, } return _.mapKeys(_.snakeCase, formatted) }, logLines) @@ -61,7 +64,7 @@ function update (deviceId, logLines) { return db.none(sql) } -function clearOldLogs () { +function clearOldLogs() { const sqls = `delete from logs where timestamp < now() - interval '3 days'; delete from server_logs @@ -69,7 +72,7 @@ function clearOldLogs () { return db.multi(sqls) } -function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) { +function getUnlimitedMachineLogs(deviceId, until = new Date().toISOString()) { // Note: sql is a little confusing here, since timestamp is used both as a column // and a reserved word, but it works. const sql = `select id, log_level, timestamp, message from logs @@ -78,14 +81,21 @@ function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) { and timestamp > (timestamp $2 - interval '2 days') order by timestamp desc, serial desc` - return Promise.all([db.any(sql, [ deviceId, until ]), getMachineName(deviceId)]) - .then(([logs, machineName]) => ({ - logs: _.map(_.mapKeys(_.camelCase), logs), - currentMachine: {deviceId, name: machineName} - })) + return Promise.all([ + db.any(sql, [deviceId, until]), + getMachineName(deviceId), + ]).then(([logs, machineName]) => ({ + logs: _.map(_.mapKeys(_.camelCase), logs), + currentMachine: { deviceId, name: machineName }, + })) } -function getMachineLogs (deviceId, until = new Date().toISOString(), limit = null, offset = 0) { +function getMachineLogs( + deviceId, + until = new Date().toISOString(), + limit = null, + offset = 0, +) { const sql = `select id, log_level, timestamp, message from logs where device_id=$1 and timestamp <= $2 @@ -93,14 +103,22 @@ function getMachineLogs (deviceId, until = new Date().toISOString(), limit = nul limit $3 offset $4` - return Promise.all([db.any(sql, [ deviceId, until, limit, offset ]), getMachineName(deviceId)]) - .then(([logs, machineName]) => ({ - logs: _.map(_.mapKeys(_.camelCase), logs), - currentMachine: {deviceId, name: machineName} - })) + return Promise.all([ + db.any(sql, [deviceId, until, limit, offset]), + getMachineName(deviceId), + ]).then(([logs, machineName]) => ({ + logs: _.map(_.mapKeys(_.camelCase), logs), + currentMachine: { deviceId, name: machineName }, + })) } -function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) { +function simpleGetMachineLogs( + deviceId, + from = new Date(0).toISOString(), + until = new Date().toISOString(), + limit = null, + offset = 0, +) { const sql = `select id, log_level, timestamp, message from logs where device_id=$1 and timestamp >= $2 @@ -109,31 +127,38 @@ function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until limit $4 offset $5` - return db.any(sql, [ deviceId, from, until, limit, offset ]) + return db + .any(sql, [deviceId, from, until, limit, offset]) .then(_.map(_.mapKeys(_.camelCase))) } -function logDateFormat (timezone, logs, fields) { +function logDateFormat(timezone, logs, fields) { return _.map(log => { - const values = _.map( - field => - { - if (_.isNil(log[field])) return null - if (!isValid(log[field])) { - logger.warn(`Tried to convert to ${timezone} timezone the value ${log[field]} and failed. Returning original value...`) - return log[field] - } - const date = utcToZonedTime(timezone, log[field]) - return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}` - }, - fields - ) + const values = _.map(field => { + if (_.isNil(log[field])) return null + if (!isValid(log[field])) { + logger.warn( + `Tried to convert to ${timezone} timezone the value ${log[field]} and failed. Returning original value...`, + ) + return log[field] + } + const date = utcToZonedTime(timezone, log[field]) + return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}` + }, fields) const fieldsToOverride = _.zipObject(fields, values) return { ...log, - ...fieldsToOverride + ...fieldsToOverride, } }, logs) } -module.exports = { getUnlimitedMachineLogs, getMachineLogs, simpleGetMachineLogs, update, getLastSeen, clearOldLogs, logDateFormat } +module.exports = { + getUnlimitedMachineLogs, + getMachineLogs, + simpleGetMachineLogs, + update, + getLastSeen, + clearOldLogs, + logDateFormat, +} diff --git a/packages/server/lib/loyalty.js b/packages/server/lib/loyalty.js index 9ac9e0b6..013e92e8 100644 --- a/packages/server/lib/loyalty.js +++ b/packages/server/lib/loyalty.js @@ -1,62 +1,66 @@ const db = require('./db') const uuid = require('uuid') const _ = require('lodash/fp') -const pgp = require('pg-promise')() -function getAvailablePromoCodes () { +function getAvailablePromoCodes() { const sql = `SELECT * FROM coupons WHERE soft_deleted=false` return db.any(sql) } -function getPromoCode (code) { +function getPromoCode(code) { const sql = `SELECT * FROM coupons WHERE code=$1 AND soft_deleted=false` return db.oneOrNone(sql, [code]) } -function createPromoCode (code, discount) { +function createPromoCode(code, discount) { const sql = `INSERT INTO coupons (id, code, discount) VALUES ($1, $2, $3) RETURNING *` return db.one(sql, [uuid.v4(), code, discount]) } -function deletePromoCode (id) { +function deletePromoCode(id) { const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1` return db.none(sql, [id]) } -function getNumberOfAvailablePromoCodes () { +function getNumberOfAvailablePromoCodes() { const sql = `SELECT COUNT(id) FROM coupons WHERE soft_deleted=false` return db.one(sql).then(res => res.count) } -function getAvailableIndividualDiscounts () { +function getAvailableIndividualDiscounts() { const sql = `SELECT * FROM individual_discounts WHERE soft_deleted=false` - return db.any(sql).then(res => _.map(it => ({ - id: it.id, - customerId: it.customer_id, - discount: it.discount - }), res)) + return db.any(sql).then(res => + _.map( + it => ({ + id: it.id, + customerId: it.customer_id, + discount: it.discount, + }), + res, + ), + ) } -function getCustomerActiveIndividualDiscount (customerId) { +function getCustomerActiveIndividualDiscount(customerId) { const sql = `SELECT * FROM individual_discounts WHERE customer_id=$1 AND soft_deleted=false LIMIT 1` return db.oneOrNone(sql, [customerId]).then(res => { if (!_.isNil(res)) { return { id: res.id, customerId: res.customer_id, - discount: res.discount + discount: res.discount, } } return res }) } -function createIndividualDiscount (customerId, discount) { +function createIndividualDiscount(customerId, discount) { const sql = `INSERT INTO individual_discounts (id, customer_id, discount) VALUES ($1, $2, $3)` return db.none(sql, [uuid.v4(), customerId, discount]) } -function deleteIndividualDiscount (id) { +function deleteIndividualDiscount(id) { const sql = `UPDATE individual_discounts SET soft_deleted=true WHERE id=$1` return db.none(sql, [id]) } @@ -70,5 +74,5 @@ module.exports = { getAvailableIndividualDiscounts, getCustomerActiveIndividualDiscount, createIndividualDiscount, - deleteIndividualDiscount + deleteIndividualDiscount, } diff --git a/packages/server/lib/machine-loader.js b/packages/server/lib/machine-loader.js index e846b026..2fd8da7c 100644 --- a/packages/server/lib/machine-loader.js +++ b/packages/server/lib/machine-loader.js @@ -13,7 +13,7 @@ const dbm = require('./postgresql_interface') const configManager = require('./new-config-manager') const notifierUtils = require('./notifier/utils') const notifierQueries = require('./notifier/queries') -const { GraphQLError } = require('graphql'); +const { GraphQLError } = require('graphql') const { loadLatestConfig } = require('./new-settings-loader') const logger = require('./logger') @@ -37,7 +37,7 @@ select d.*, COALESCE(emptybills, 0) + COALESCE(regularbills, 0) as cashbox from group by cit.device_id ) as nbills on nbills.device_id = d.device_id` -function toMachineObject (r) { +function toMachineObject(r) { return { deviceId: r.device_id, cashUnits: { @@ -51,48 +51,56 @@ function toMachineObject (r) { recycler3: r.recycler3, recycler4: r.recycler4, recycler5: r.recycler5, - recycler6: r.recycler6 + recycler6: r.recycler6, }, numberOfCassettes: r.number_of_cassettes, numberOfRecyclers: r.number_of_recyclers, version: r.version, model: r.model, diagnostics: { - timestamp: r.diagnostics_timestamp? new Date(r.diagnostics_timestamp) : null, - scanTimestamp: r.diagnostics_scan_timestamp? new Date(r.diagnostics_scan_timestamp) : null, - frontTimestamp: r.diagnostics_front_timestamp? new Date(r.diagnostics_front_timestamp) : null + timestamp: r.diagnostics_timestamp + ? new Date(r.diagnostics_timestamp) + : null, + scanTimestamp: r.diagnostics_scan_timestamp + ? new Date(r.diagnostics_scan_timestamp) + : null, + frontTimestamp: r.diagnostics_front_timestamp + ? new Date(r.diagnostics_front_timestamp) + : null, }, pairedAt: new Date(r.created), lastPing: new Date(r.last_online), name: r.name, - paired: r.paired + paired: r.paired, // TODO: we shall start using this JSON field at some point // location: r.location, } } -function getMachineIds () { +function getMachineIds() { const sql = 'select device_id from devices' return db.any(sql) } -function getMachines () { +function getMachines() { const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} where display=TRUE ORDER BY created` - return db.any(sql) - .then(rr => rr.map(toMachineObject)) + return db.any(sql).then(rr => rr.map(toMachineObject)) } -function getUnpairedMachines () { - return db.any('SELECT * FROM unpaired_devices') - .then(_.map(r => - _.flow( - _.set('deviceId', _.get('device_id', r)), - _.unset('device_id') - )(r) - )) +function getUnpairedMachines() { + return db + .any('SELECT * FROM unpaired_devices') + .then( + _.map(r => + _.flow( + _.set('deviceId', _.get('device_id', r)), + _.unset('device_id'), + )(r), + ), + ) } -function getConfig (defaultConfig) { +function getConfig(defaultConfig) { return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig() } @@ -104,7 +112,7 @@ const getStatus = (ping, stuck) => { return fullyFunctionalStatus } -function addName (pings, events, config) { +function addName(pings, events, config) { return machine => { const cashOutConfig = configManager.getCashOut(machine.deviceId, config) @@ -113,22 +121,38 @@ function addName (pings, events, config) { const statuses = [ getStatus( _.first(pings[machine.deviceId]), - _.first(checkStuckScreen(events, machine)) - ) + _.first(checkStuckScreen(events, machine)), + ), ] return _.assign(machine, { cashOut, statuses }) } } -function getMachineNames (config) { - return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()]) - .then(([rawMachines, config, heartbeat, performance]) => Promise.all( - [rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance] - )) +function getMachineNames(config) { + return Promise.all([ + getMachines(), + getConfig(config), + getNetworkHeartbeat(), + getNetworkPerformance(), + ]) + .then(([rawMachines, config, heartbeat, performance]) => + Promise.all([ + rawMachines, + checkPings(rawMachines), + dbm.machineEvents(), + config, + heartbeat, + performance, + ]), + ) .then(([rawMachines, pings, events, config, heartbeat, performance]) => { - const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y))) - const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance) + const mergeByDeviceId = (x, y) => + _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y))) + const machines = mergeByDeviceId( + mergeByDeviceId(rawMachines, heartbeat), + performance, + ) return machines.map(addName(pings, events, config)) }) @@ -144,67 +168,133 @@ function getMachineNames (config) { * @param {string} machineId machine id * @returns {string} machine name */ -function getMachineName (machineId) { +function getMachineName(machineId) { const sql = 'SELECT name FROM devices WHERE device_id=$1' - return db.oneOrNone(sql, [machineId]) - .then(it => it?.name) + return db.oneOrNone(sql, [machineId]).then(it => it?.name) } -function getMachine (machineId, config) { +function getMachine(machineId, config) { const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1` const queryMachine = db.oneOrNone(sql, [machineId]).then(r => { - if (r === null) throw new GraphQLError('Resource doesn\'t exist', { extensions: { code: 'NOT_FOUND' } }) + if (r === null) + throw new GraphQLError("Resource doesn't exist", { + extensions: { code: 'NOT_FOUND' }, + }) else return toMachineObject(r) }) - return Promise.all([queryMachine, dbm.machineEvents(), config, getNetworkHeartbeatByDevice(machineId), getNetworkPerformanceByDevice(machineId)]) - .then(([machine, events, config, heartbeat, performance]) => { - const pings = checkPings([machine]) - const mergedMachine = { - ...machine, - responseTime: _.get('responseTime', heartbeat), - packetLoss: _.get('packetLoss', heartbeat), - downloadSpeed: _.get('downloadSpeed', performance), - } + return Promise.all([ + queryMachine, + dbm.machineEvents(), + config, + getNetworkHeartbeatByDevice(machineId), + getNetworkPerformanceByDevice(machineId), + ]).then(([machine, events, config, heartbeat, performance]) => { + const pings = checkPings([machine]) + const mergedMachine = { + ...machine, + responseTime: _.get('responseTime', heartbeat), + packetLoss: _.get('packetLoss', heartbeat), + downloadSpeed: _.get('downloadSpeed', performance), + } - return addName(pings, events, config)(mergedMachine) - }) + return addName(pings, events, config)(mergedMachine) + }) } -function renameMachine (rec) { +function renameMachine(rec) { const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2' return db.none(sql, [rec.newName, rec.deviceId]) } -function resetCashOutBills (rec) { +function resetCashOutBills(rec) { const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId }) - const { cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits + const { + cassette1, + cassette2, + cassette3, + cassette4, + recycler1, + recycler2, + recycler3, + recycler4, + recycler5, + recycler6, + } = rec.cashUnits const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11;` - return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance')) + return db + .none(sql, [ + cassette1, + cassette2, + cassette3, + cassette4, + recycler1, + recycler2, + recycler3, + recycler4, + recycler5, + recycler6, + rec.deviceId, + ]) + .then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance')) } -function setCassetteBills (rec) { - const { cashbox, cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits - return getMachine(rec.deviceId) - .then(machine => { - const oldCashboxCount = machine?.cashUnits?.cashbox - if (_.isNil(oldCashboxCount) || cashbox.toString() === oldCashboxCount.toString()) { - const sql = ` +function setCassetteBills(rec) { + const { + cashbox, + cassette1, + cassette2, + cassette3, + cassette4, + recycler1, + recycler2, + recycler3, + recycler4, + recycler5, + recycler6, + } = rec.cashUnits + return getMachine(rec.deviceId).then(machine => { + const oldCashboxCount = machine?.cashUnits?.cashbox + if ( + _.isNil(oldCashboxCount) || + cashbox.toString() === oldCashboxCount.toString() + ) { + const sql = ` UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3), recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6) WHERE device_id=$11` - return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId]) - } + return db.none(sql, [ + cassette1, + cassette2, + cassette3, + cassette4, + recycler1, + recycler2, + recycler3, + recycler4, + recycler5, + recycler6, + rec.deviceId, + ]) + } - return batching.updateMachineWithBatch({ ...rec, oldCashboxValue: oldCashboxCount }) + return batching.updateMachineWithBatch({ + ...rec, + oldCashboxValue: oldCashboxCount, }) + }) } -function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) { +function emptyMachineUnits({ deviceId, newUnits, fiatCode }) { return loadLatestConfig() - .then(config => Promise.all([getMachine(deviceId), configManager.getCashOut(deviceId, config)])) + .then(config => + Promise.all([ + getMachine(deviceId), + configManager.getCashOut(deviceId, config), + ]), + ) .then(([machine, cashoutSettings]) => { const movedBills = _.reduce( (acc, value) => ({ @@ -212,34 +302,42 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) { [value]: { operationName: `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, delta: newUnits[value] - machine.cashUnits[value], - denomination: value !== 'cashbox' ? cashoutSettings[value] : null - } + denomination: value !== 'cashbox' ? cashoutSettings[value] : null, + }, }), {}, - _.keys(newUnits) + _.keys(newUnits), ) - const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills))) + const operationNames = _.mapValues(it => it.operationName)( + _.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills)), + ) const operationsToCreate = _.map(it => ({ id: uuid.v4(), device_id: deviceId, - operation_type: it + operation_type: it, }))(operationNames) const billArr = _.reduce( (acc, value) => { const unit = movedBills[value] - return _.concat(acc, _.times(() => ({ - id: uuid.v4(), - fiat: unit.denomination, - fiat_code: fiatCode, - device_id: deviceId - // TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event - // cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id - }), Math.abs(unit.delta))) + return _.concat( + acc, + _.times( + () => ({ + id: uuid.v4(), + fiat: unit.denomination, + fiat_code: fiatCode, + device_id: deviceId, + // TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event + // cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id + }), + Math.abs(unit.delta), + ), + ) }, [], - _.keys(_.omit(['cashbox'], movedBills)) + _.keys(_.omit(['cashbox'], movedBills)), ) // This occurs when an empty unit is called when the units are already empty, hence, no bills moved around @@ -249,59 +347,76 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) { return db.tx(t => { const q1Cols = ['id', 'device_id', 'operation_type'] - const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation')) + const q1 = t.none( + pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'), + ) const q2Cols = ['id', 'fiat', 'fiat_code', 'device_id'] - const q2 = t.none(pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills')) - const q3 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [ - _.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1), - _.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2), - _.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3), - _.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4), - _.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1), - _.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2), - _.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3), - _.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4), - _.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5), - _.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6), - deviceId - ]) + const q2 = t.none( + pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'), + ) + const q3 = t.none( + `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, + [ + _.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1), + _.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2), + _.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3), + _.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4), + _.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1), + _.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2), + _.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3), + _.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4), + _.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5), + _.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6), + deviceId, + ], + ) return t.batch([q1, q2, q3]) }) }) } -function refillMachineUnits ({ deviceId, newUnits }) { - return getMachine(deviceId) - .then(machine => { - const movedBills = _.reduce( - (acc, value) => ({ - ...acc, - [value]: { - operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`, - delta: newUnits[value] - machine.cashUnits[value] - } - }), - {}, - _.keys(newUnits) +function refillMachineUnits({ deviceId, newUnits }) { + return getMachine(deviceId).then(machine => { + const movedBills = _.reduce( + (acc, value) => ({ + ...acc, + [value]: { + operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`, + delta: newUnits[value] - machine.cashUnits[value], + }, + }), + {}, + _.keys(newUnits), + ) + + const operationNames = _.mapValues(it => it.operationName)( + _.filter(it => Math.abs(it.delta) > 0)( + _.omit( + ['cassette1', 'cassette2', 'cassette3', 'cassette4'], + movedBills, + ), + ), + ) + const operationsToCreate = _.map(it => ({ + id: uuid.v4(), + device_id: deviceId, + operation_type: it, + }))(operationNames) + + // This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around + if (_.isEmpty(operationsToCreate)) { + return Promise.resolve() + } + + return db.tx(t => { + const q1Cols = ['id', 'device_id', 'operation_type'] + const q1 = t.none( + pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'), ) - - const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills))) - const operationsToCreate = _.map(it => ({ - id: uuid.v4(), - device_id: deviceId, - operation_type: it - }))(operationNames) - - // This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around - if (_.isEmpty(operationsToCreate)) { - return Promise.resolve() - } - - return db.tx(t => { - const q1Cols = ['id', 'device_id', 'operation_type'] - const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation')) - const q2 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [ + const q2 = t.none( + `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, + [ _.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1), _.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2), _.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3), @@ -312,64 +427,70 @@ function refillMachineUnits ({ deviceId, newUnits }) { _.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4), _.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5), _.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6), - deviceId - ]) - - return t.batch([q1, q2]) - }) + deviceId, + ], + ) + + return t.batch([q1, q2]) }) + }) } -function unpair (rec) { +function unpair(rec) { return pairing.unpair(rec.deviceId) } -function reboot (rec) { - return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { +function reboot(rec) { + return db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ action: 'reboot', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )]) + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]) } -function shutdown (rec) { - return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { +function shutdown(rec) { + return db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ action: 'shutdown', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )]) + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]) } -function restartServices (rec) { - return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { +function restartServices(rec) { + return db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ action: 'restartServices', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )]) + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]) } -function emptyUnit (rec) { - return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { +function emptyUnit(rec) { + return db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ action: 'emptyUnit', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )]) + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]) } -function refillUnit (rec) { - return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { +function refillUnit(rec) { + return db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ action: 'refillUnit', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )]) + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]) } -function diagnostics (rec) { +function diagnostics(rec) { const directory = `${OPERATOR_DATA_DIR}/diagnostics/${rec.deviceId}/` const sql = `UPDATE devices SET diagnostics_timestamp = NULL, @@ -391,48 +512,65 @@ function diagnostics (rec) { return Promise.all(removeFiles) .then(() => db.none(sql, [rec.deviceId])) - .then(() => db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify( - { - action: 'diagnostics', - value: _.pick(['deviceId', 'operatorId', 'action'], rec) - } - )])) + .then(() => + db.none('NOTIFY $1:name, $2', [ + 'machineAction', + JSON.stringify({ + action: 'diagnostics', + value: _.pick(['deviceId', 'operatorId', 'action'], rec), + }), + ]), + ) } -function setMachine (rec, operatorId) { +function setMachine(rec, operatorId) { rec.operatorId = operatorId switch (rec.action) { - case 'rename': return renameMachine(rec) - case 'resetCashOutBills': return resetCashOutBills(rec) - case 'setCassetteBills': return setCassetteBills(rec) - case 'unpair': return unpair(rec) - case 'reboot': return reboot(rec) - case 'shutdown': return shutdown(rec) - case 'restartServices': return restartServices(rec) - case 'emptyUnit': return emptyUnit(rec) - case 'refillUnit': return refillUnit(rec) - case 'diagnostics': return diagnostics(rec) - default: throw new Error('No such action: ' + rec.action) + case 'rename': + return renameMachine(rec) + case 'resetCashOutBills': + return resetCashOutBills(rec) + case 'setCassetteBills': + return setCassetteBills(rec) + case 'unpair': + return unpair(rec) + case 'reboot': + return reboot(rec) + case 'shutdown': + return shutdown(rec) + case 'restartServices': + return restartServices(rec) + case 'emptyUnit': + return emptyUnit(rec) + case 'refillUnit': + return refillUnit(rec) + case 'diagnostics': + return diagnostics(rec) + default: + throw new Error('No such action: ' + rec.action) } } -function updateNetworkPerformance (deviceId, data) { +function updateNetworkPerformance(deviceId, data) { if (_.isEmpty(data)) return Promise.resolve(true) const downloadSpeed = _.head(data) const dbData = { device_id: deviceId, download_speed: downloadSpeed.speed, - created: new Date() + created: new Date(), } - const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'], - { table: 'machine_network_performance' }) - const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' + + const cs = new pgp.helpers.ColumnSet( + ['device_id', 'download_speed', 'created'], + { table: 'machine_network_performance' }, + ) + const onConflict = + ' ON CONFLICT (device_id) DO UPDATE SET ' + cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] }) const upsert = pgp.helpers.insert(dbData, cs) + onConflict return db.none(upsert) } -function updateNetworkHeartbeat (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) @@ -440,41 +578,47 @@ function updateNetworkHeartbeat (deviceId, data) { id: uuid.v4(), device_id: deviceId, average_response_time: avgResponseTime, - average_packet_loss: avgPacketLoss + average_packet_loss: avgPacketLoss, } const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat') return db.none(sql) } -function getNetworkPerformance () { +function getNetworkPerformance() { const sql = `SELECT device_id, download_speed FROM machine_network_performance` - return db.manyOrNone(sql) - .then(res => _.map(_.mapKeys(_.camelCase))(res)) + return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res)) } -function getNetworkHeartbeat () { +function getNetworkHeartbeat() { const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id FROM machine_network_heartbeat GROUP BY device_id` - return db.manyOrNone(sql) - .then(res => _.map(_.mapKeys(_.camelCase))(res)) + return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res)) } -function getNetworkPerformanceByDevice (deviceId) { +function getNetworkPerformanceByDevice(deviceId) { const sql = `SELECT device_id, download_speed FROM machine_network_performance WHERE device_id = $1` - return db.manyOrNone(sql, [deviceId]) - .then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res))) + return db.manyOrNone(sql, [deviceId]).then(res => + _.mapKeys( + _.camelCase, + _.find(it => it.device_id === deviceId, res), + ), + ) } -function getNetworkHeartbeatByDevice (deviceId) { +function getNetworkHeartbeatByDevice(deviceId) { const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id FROM machine_network_heartbeat WHERE device_id = $1 GROUP BY device_id` - return db.manyOrNone(sql, [deviceId]) - .then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res))) + return db.manyOrNone(sql, [deviceId]).then(res => + _.mapKeys( + _.camelCase, + _.find(it => it.device_id === deviceId, res), + ), + ) } -function updateDiagnostics (deviceId, images) { +function updateDiagnostics(deviceId, images) { const sql = `UPDATE devices SET diagnostics_timestamp = NOW(), diagnostics_scan_updated_at = CASE WHEN $2 THEN NOW() ELSE diagnostics_scan_updated_at END, @@ -484,19 +628,25 @@ function updateDiagnostics (deviceId, images) { const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/` const { scan, front } = images - return updatePhotos(directory, [['scan.jpg', scan], ['front.jpg', front]]) + return updatePhotos(directory, [ + ['scan.jpg', scan], + ['front.jpg', front], + ]) .then(() => db.none(sql, [deviceId, !!scan, !!front])) .catch(err => logger.error('while running machine diagnostics: ', err)) } const updateFailedQRScans = (deviceId, frames) => { - const timestamp = (new Date()).toISOString() + const timestamp = new Date().toISOString() const directory = `${OPERATOR_DATA_DIR}/failedQRScans/${deviceId}/` - const filenames = _.map(no => `${timestamp}-${no}.jpg`, _.range(0, _.size(frames))) + const filenames = _.map( + no => `${timestamp}-${no}.jpg`, + _.range(0, _.size(frames)), + ) return updatePhotos(directory, _.zip(filenames, frames)) } -function createPhoto (name, data, dir) { +function createPhoto(name, data, dir) { if (!data) { logger.error(`Diagnostics error: No data to save for ${name} photo`) return Promise.resolve() @@ -507,12 +657,12 @@ function createPhoto (name, data, dir) { return fsPromises.writeFile(filename, decodedImageData) } -function updatePhotos (dir, photoPairs) { +function updatePhotos(dir, photoPairs) { const dirname = path.join(dir) _.attempt(() => makeDir.sync(dirname)) - return Promise.all(photoPairs.map( - ([filename, data]) => createPhoto(filename, data, dirname) - )) + return Promise.all( + photoPairs.map(([filename, data]) => createPhoto(filename, data, dirname)), + ) } module.exports = { @@ -530,5 +680,5 @@ module.exports = { emptyMachineUnits, refillMachineUnits, updateDiagnostics, - updateFailedQRScans + updateFailedQRScans, } diff --git a/packages/server/lib/middlewares/authorize.js b/packages/server/lib/middlewares/authorize.js index a76a7438..1a4d24df 100644 --- a/packages/server/lib/middlewares/authorize.js +++ b/packages/server/lib/middlewares/authorize.js @@ -2,7 +2,8 @@ const pairing = require('../pairing') const logger = require('../logger') const authorize = function (req, res, next) { - return pairing.isPaired(req.deviceId) + return pairing + .isPaired(req.deviceId) .then(deviceName => { if (deviceName) { req.deviceName = deviceName diff --git a/packages/server/lib/middlewares/ca.js b/packages/server/lib/middlewares/ca.js index 04b3dbc0..a0f65768 100644 --- a/packages/server/lib/middlewares/ca.js +++ b/packages/server/lib/middlewares/ca.js @@ -1,10 +1,11 @@ const pairing = require('../pairing') const logger = require('../logger') -function ca (req, res) { +function ca(req, res) { const token = req.query.token - return pairing.authorizeCaDownload(token) + return pairing + .authorizeCaDownload(token) .then(ca => res.json({ ca })) .catch(error => { logger.error(error.message) diff --git a/packages/server/lib/middlewares/errorHandler.js b/packages/server/lib/middlewares/errorHandler.js index ac695263..1c5b742c 100644 --- a/packages/server/lib/middlewares/errorHandler.js +++ b/packages/server/lib/middlewares/errorHandler.js @@ -1,9 +1,7 @@ const logger = require('../logger') -function errorHandler (err, req, res, next) { - const statusCode = err.name === 'HTTPError' - ? err.code || 500 - : 500 +function errorHandler(err, req, res) { + const statusCode = err.name === 'HTTPError' ? err.code || 500 : 500 const json = { error: err.message } diff --git a/packages/server/lib/middlewares/filterOldRequests.js b/packages/server/lib/middlewares/filterOldRequests.js index 601207b6..b4f041ce 100644 --- a/packages/server/lib/middlewares/filterOldRequests.js +++ b/packages/server/lib/middlewares/filterOldRequests.js @@ -5,19 +5,23 @@ const CLOCK_SKEW = 60 * 1000 const REQUEST_TTL = 3 * 60 * 1000 const THROTTLE_CLOCK_SKEW = 60 * 1000 -function filterOldRequests (req, res, next) { +function filterOldRequests(req, res, next) { const deviceTime = req.deviceTime const deviceId = req.deviceId const timestamp = Date.now() const delta = timestamp - Date.parse(deviceTime) - const shouldTrigger = !state.canLogClockSkewMap[deviceId] || + const shouldTrigger = + !state.canLogClockSkewMap[deviceId] || timestamp - state.canLogClockSkewMap[deviceId] >= THROTTLE_CLOCK_SKEW if (delta > CLOCK_SKEW && shouldTrigger) { state.canLogClockSkewMap[deviceId] = timestamp - logger.error('Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock', - req.deviceName, (delta / 1000).toFixed(2)) + logger.error( + 'Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock', + req.deviceName, + (delta / 1000).toFixed(2), + ) } if (delta > REQUEST_TTL) return res.status(408).json({ error: 'stale' }) diff --git a/packages/server/lib/middlewares/operatorId.js b/packages/server/lib/middlewares/operatorId.js index d11d22f6..057046ea 100644 --- a/packages/server/lib/middlewares/operatorId.js +++ b/packages/server/lib/middlewares/operatorId.js @@ -1,6 +1,6 @@ const { getOperatorId } = require('../operator') -function findOperatorId (req, res, next) { +function findOperatorId(req, res, next) { return getOperatorId('middleware') .then(operatorId => { res.locals.operatorId = operatorId diff --git a/packages/server/lib/middlewares/populateDeviceId.js b/packages/server/lib/middlewares/populateDeviceId.js index 884cc436..9c139e4e 100644 --- a/packages/server/lib/middlewares/populateDeviceId.js +++ b/packages/server/lib/middlewares/populateDeviceId.js @@ -1,6 +1,6 @@ const crypto = require('crypto') -function sha256 (buf) { +function sha256(buf) { if (!buf) return null const hash = crypto.createHash('sha256') @@ -9,10 +9,13 @@ function sha256 (buf) { } const populateDeviceId = function (req, res, next) { - const peerCert = req.socket.getPeerCertificate ? req.socket.getPeerCertificate() : null + const peerCert = req.socket.getPeerCertificate + ? req.socket.getPeerCertificate() + : null const deviceId = peerCert?.raw ? sha256(peerCert.raw) : null - - if (!deviceId) return res.status(500).json({ error: 'Unable to find certificate' }) + + if (!deviceId) + return res.status(500).json({ error: 'Unable to find certificate' }) req.deviceId = deviceId req.deviceTime = req.get('date') diff --git a/packages/server/lib/middlewares/populateSettings.js b/packages/server/lib/middlewares/populateSettings.js index 18add0d1..6a34ca3d 100644 --- a/packages/server/lib/middlewares/populateSettings.js +++ b/packages/server/lib/middlewares/populateSettings.js @@ -3,57 +3,74 @@ const state = require('./state') const newSettingsLoader = require('../new-settings-loader') const logger = require('../logger') -db.connect({ direct: true }).then(sco => { - sco.client.on('notification', data => { - const parsedData = JSON.parse(data.payload) - return reload(parsedData.operatorId) +db.connect({ direct: true }) + .then(sco => { + sco.client.on('notification', data => { + const parsedData = JSON.parse(data.payload) + return reload(parsedData.operatorId) + }) + return sco.none('LISTEN $1:name', 'reload') }) - return sco.none('LISTEN $1:name', 'reload') -}).catch(console.error) + .catch(console.error) -db.connect({ direct: true }).then(sco => { - sco.client.on('notification', data => { - const parsedData = JSON.parse(data.payload) - return machineAction(parsedData.action, parsedData.value) +db.connect({ direct: true }) + .then(sco => { + sco.client.on('notification', data => { + const parsedData = JSON.parse(data.payload) + return machineAction(parsedData.action, parsedData.value) + }) + return sco.none('LISTEN $1:name', 'machineAction') }) - return sco.none('LISTEN $1:name', 'machineAction') -}).catch(console.error) + .catch(console.error) -function machineAction (type, value) { +function machineAction(type, value) { const deviceId = value.deviceId const operatorId = value.operatorId const pid = state.pids?.[operatorId]?.[deviceId]?.pid switch (type) { case 'reboot': - logger.debug(`Rebooting machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Rebooting machine '${deviceId}' from operator ${operatorId}`, + ) state.reboots[operatorId] = { [deviceId]: pid } break case 'shutdown': - logger.debug(`Shutting down machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Shutting down machine '${deviceId}' from operator ${operatorId}`, + ) state.shutdowns[operatorId] = { [deviceId]: pid } break case 'restartServices': - logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Restarting services of machine '${deviceId}' from operator ${operatorId}`, + ) state.restartServicesMap[operatorId] = { [deviceId]: pid } break case 'emptyUnit': - logger.debug(`Emptying units from machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Emptying units from machine '${deviceId}' from operator ${operatorId}`, + ) state.emptyUnit[operatorId] = { [deviceId]: pid } break case 'refillUnit': - logger.debug(`Refilling recyclers from machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Refilling recyclers from machine '${deviceId}' from operator ${operatorId}`, + ) state.refillUnit[operatorId] = { [deviceId]: pid } break case 'diagnostics': - logger.debug(`Running diagnostics on machine '${deviceId}' from operator ${operatorId}`) + logger.debug( + `Running diagnostics on machine '${deviceId}' from operator ${operatorId}`, + ) state.diagnostics[operatorId] = { [deviceId]: pid } + break default: break } } -function reload (operatorId) { +function reload(operatorId) { state.needsSettingsReload[operatorId] = true } @@ -73,11 +90,14 @@ const populateSettings = function (req, res, next) { // 4. There's no cached config, cache and send the latest config if (versionId) { - const cachedVersionedSettings = settingsCache.get(`${operatorId}-v${versionId}`) + const cachedVersionedSettings = settingsCache.get( + `${operatorId}-v${versionId}`, + ) if (!cachedVersionedSettings) { logger.debug('Fetching a specific config version cached value') - return newSettingsLoader.load(versionId) + return newSettingsLoader + .load(versionId) .then(settings => { settingsCache.set(`${operatorId}-v${versionId}`, settings) req.settings = settings @@ -94,16 +114,22 @@ const populateSettings = function (req, res, next) { const operatorSettings = settingsCache.get(`${operatorId}-latest`) if (!!needsSettingsReload[operatorId] || !operatorSettings) { - !!needsSettingsReload[operatorId] - ? logger.debug('Fetching and caching a new latest config value, as a reload was requested') - : logger.debug('Fetching the latest config version because there\'s no cached value') + needsSettingsReload[operatorId] + ? logger.debug( + 'Fetching and caching a new latest config value, as a reload was requested', + ) + : logger.debug( + "Fetching the latest config version because there's no cached value", + ) - return newSettingsLoader.loadLatest() + return newSettingsLoader + .loadLatest() .then(settings => { const versionId = settings.version settingsCache.set(`${operatorId}-latest`, settings) settingsCache.set(`${operatorId}-v${versionId}`, settings) - if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId] + if (needsSettingsReload[operatorId]) + delete needsSettingsReload[operatorId] req.settings = settings }) .then(() => next()) diff --git a/packages/server/lib/middlewares/rejectIncompatbleMachines.js b/packages/server/lib/middlewares/rejectIncompatbleMachines.js index 48ecef6f..0086e464 100644 --- a/packages/server/lib/middlewares/rejectIncompatbleMachines.js +++ b/packages/server/lib/middlewares/rejectIncompatbleMachines.js @@ -12,20 +12,24 @@ const rejectIncompatibleMachines = function (req, res, next) { const machineMajor = semver.major(machineVersion) if (serverMajor - machineMajor > 1) { - logger.error(`Machine version too old: ${machineVersion} deviceId: ${deviceId}`) + logger.error( + `Machine version too old: ${machineVersion} deviceId: ${deviceId}`, + ) return res.status(400).json({ - error: 'Machine version too old' + error: 'Machine version too old', }) } if (serverMajor < machineMajor) { - logger.error(`Machine version too new: ${machineVersion} deviceId: ${deviceId}`) + logger.error( + `Machine version too new: ${machineVersion} deviceId: ${deviceId}`, + ) return res.status(400).json({ - error: 'Machine version too new' + error: 'Machine version too new', }) } next() } -module.exports = rejectIncompatibleMachines \ No newline at end of file +module.exports = rejectIncompatibleMachines diff --git a/packages/server/lib/middlewares/state.js b/packages/server/lib/middlewares/state.js index ee5da8e6..b6419d0f 100644 --- a/packages/server/lib/middlewares/state.js +++ b/packages/server/lib/middlewares/state.js @@ -7,7 +7,7 @@ module.exports = (function () { needsSettingsReload: {}, settingsCache: new NodeCache({ stdTTL: SETTINGS_CACHE_REFRESH, - checkperiod: SETTINGS_CACHE_REFRESH // Clear cache every hour + checkperiod: SETTINGS_CACHE_REFRESH, // Clear cache every hour }), canLogClockSkewMap: {}, canGetLastSeenMap: {}, @@ -18,6 +18,6 @@ module.exports = (function () { emptyUnit: {}, refillUnit: {}, diagnostics: {}, - mnemonic: null + mnemonic: null, } -}()) +})() diff --git a/packages/server/lib/migrate.js b/packages/server/lib/migrate.js index 610f926e..0313307d 100644 --- a/packages/server/lib/migrate.js +++ b/packages/server/lib/migrate.js @@ -7,11 +7,11 @@ const migrateDir = path.resolve(__dirname, '..', 'migrations') const migrateOpts = { migrationsDirectory: migrateDir, stateStore: new DbMigrateStore(), - filterFunction: it => it.match(/^\d+.*\.js$/) + filterFunction: it => it.match(/^\d+.*\.js$/), } module.exports = { run } -function run () { +function run() { return new Promise((resolve, reject) => { migrate.load(migrateOpts, (err, set) => { if (err) return reject(err) diff --git a/packages/server/lib/mnemonic-helpers.js b/packages/server/lib/mnemonic-helpers.js index ad4e3675..1a308235 100644 --- a/packages/server/lib/mnemonic-helpers.js +++ b/packages/server/lib/mnemonic-helpers.js @@ -1,7 +1,7 @@ const bip39 = require('bip39') const os = require('os') -function fromSeed (seed) { +function fromSeed(seed) { const words = bip39.entropyToMnemonic(seed).split(' ') let mnemonic = '' @@ -11,7 +11,7 @@ function fromSeed (seed) { return mnemonic } -function toEntropyBuffer (mnemonic) { +function toEntropyBuffer(mnemonic) { const hex = bip39.mnemonicToEntropy(mnemonic.split('\n').join(' ').trim()) return Buffer.from(hex.trim(), 'hex') } diff --git a/packages/server/lib/new-admin/admin-server.js b/packages/server/lib/new-admin/admin-server.js index 3b759bcf..03153dc4 100644 --- a/packages/server/lib/new-admin/admin-server.js +++ b/packages/server/lib/new-admin/admin-server.js @@ -9,8 +9,12 @@ const nocache = require('nocache') const cookieParser = require('cookie-parser') const { ApolloServer } = require('@apollo/server') const { expressMiddleware } = require('@apollo/server/express4') -const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled') -const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default') +const { + ApolloServerPluginLandingPageDisabled, +} = require('@apollo/server/plugin/disabled') +const { + ApolloServerPluginLandingPageLocalDefault, +} = require('@apollo/server/plugin/landingPage/default') const { mergeResolvers } = require('@graphql-tools/merge') const { makeExecutableSchema } = require('@graphql-tools/schema') @@ -23,7 +27,11 @@ const { authDirectiveTransformer } = require('./graphql/directives') const { typeDefs, resolvers } = require('./graphql/schema') const findOperatorId = require('../middlewares/operatorId') const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants') -const { session, cleanUserSessions, buildApolloContext } = require('./middlewares') +const { + session, + cleanUserSessions, + buildApolloContext, +} = require('./middlewares') const devMode = require('minimist')(process.argv.slice(2)).dev @@ -55,8 +63,12 @@ const loadRoutes = async () => { app.use(session) // Dynamic import for graphql-upload since it's not a CommonJS module - const { default: graphqlUploadExpress } = await import('graphql-upload/graphqlUploadExpress.mjs') - const { default: GraphQLUpload } = await import('graphql-upload/GraphQLUpload.mjs') + const { default: graphqlUploadExpress } = await import( + 'graphql-upload/graphqlUploadExpress.mjs' + ) + const { default: GraphQLUpload } = await import( + 'graphql-upload/GraphQLUpload.mjs' + ) app.use(graphqlUploadExpress()) @@ -75,29 +87,33 @@ const loadRoutes = async () => { return formattedError }, plugins: [ - devMode - ? ApolloServerPluginLandingPageLocalDefault() - : ApolloServerPluginLandingPageDisabled() - ] + devMode + ? ApolloServerPluginLandingPageLocalDefault() + : ApolloServerPluginLandingPageDisabled(), + ], }) - await apolloServer.start(); + await apolloServer.start() app.use( '/graphql', express.json(), expressMiddleware(apolloServer, { - context: async ({ req, res }) => buildApolloContext({ req, res }) - }) - ); - + context: async ({ req, res }) => buildApolloContext({ req, res }), + }), + ) 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( + '/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'))) + app.get('*', (req, res) => + res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')), + ) return app } @@ -105,10 +121,10 @@ const loadRoutes = async () => { const certOptions = { key: fs.readFileSync(KEY_PATH), cert: fs.readFileSync(CERT_PATH), - ca: fs.readFileSync(CA_PATH) + ca: fs.readFileSync(CA_PATH), } -async function run () { +async function run() { const app = await loadRoutes() const serverPort = devMode ? 8070 : 443 diff --git a/packages/server/lib/new-admin/config/accounts.js b/packages/server/lib/new-admin/config/accounts.js index aed637d9..87a3e35a 100644 --- a/packages/server/lib/new-admin/config/accounts.js +++ b/packages/server/lib/new-admin/config/accounts.js @@ -3,8 +3,10 @@ const _ = require('lodash/fp') const { ALL } = require('../../plugins/common/ccxt') -const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS -const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL +const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = + COINS +const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = + ALL const TICKER = 'ticker' const WALLET = 'wallet' @@ -18,39 +20,142 @@ const WALLET_SCORING = 'wallet_scoring' const COMPLIANCE = 'compliance' const ALL_ACCOUNTS = [ - { code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO }, - { code: 'bitfinex', display: 'Bitfinex', class: EXCHANGE, cryptos: bitfinex.CRYPTO }, - { code: 'binance', display: 'Binance', class: TICKER, cryptos: binance.CRYPTO }, - { code: 'binanceus', display: 'Binance.us', class: TICKER, cryptos: binanceus.CRYPTO }, + { + code: 'bitfinex', + display: 'Bitfinex', + class: TICKER, + cryptos: bitfinex.CRYPTO, + }, + { + code: 'bitfinex', + display: 'Bitfinex', + class: EXCHANGE, + cryptos: bitfinex.CRYPTO, + }, + { + code: 'binance', + display: 'Binance', + class: TICKER, + cryptos: binance.CRYPTO, + }, + { + code: 'binanceus', + display: 'Binance.us', + class: TICKER, + cryptos: binanceus.CRYPTO, + }, { code: 'cex', display: 'CEX.IO', class: TICKER, cryptos: cex.CRYPTO }, { code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO }, { code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.CRYPTO }, - { code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: bitstamp.CRYPTO }, + { + code: 'bitstamp', + display: 'Bitstamp', + class: TICKER, + cryptos: bitstamp.CRYPTO, + }, { code: 'itbit', display: 'itBit', class: TICKER, cryptos: itbit.CRYPTO }, - { code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true }, + { + code: 'mock-ticker', + display: 'Mock (Caution!)', + class: TICKER, + cryptos: ALL_CRYPTOS, + dev: true, + }, { code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] }, - { code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS }, - { code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT, USDC] }, - { code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] }, - { code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] }, + { + code: 'no-layer2', + display: 'No Layer 2', + class: LAYER_2, + cryptos: ALL_CRYPTOS, + }, + { + code: 'infura', + display: 'Infura/Alchemy', + class: WALLET, + cryptos: [ETH, USDT, USDC], + }, + { + code: 'trongrid', + display: 'Trongrid', + class: WALLET, + cryptos: [TRX, USDT_TRON], + }, + { + code: 'geth', + display: 'geth (deprecated)', + class: WALLET, + cryptos: [ETH, USDT, USDC], + }, { code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] }, { code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] }, { code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] }, { code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] }, - { code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] }, - { code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] }, + { + code: 'bitcoincashd', + display: 'bitcoincashd', + class: WALLET, + cryptos: [BCH], + }, + { + code: 'bitgo', + display: 'BitGo', + class: WALLET, + cryptos: [BTC, ZEC, LTC, BCH, DASH], + }, { code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] }, - { code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO }, + { + code: 'bitstamp', + display: 'Bitstamp', + class: EXCHANGE, + cryptos: bitstamp.CRYPTO, + }, { code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO }, - { code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO }, - { code: 'binance', display: 'Binance', class: EXCHANGE, cryptos: binance.CRYPTO }, - { code: 'binanceus', display: 'Binance.us', class: EXCHANGE, cryptos: binanceus.CRYPTO }, + { + code: 'kraken', + display: 'Kraken', + class: EXCHANGE, + cryptos: kraken.CRYPTO, + }, + { + code: 'binance', + display: 'Binance', + class: EXCHANGE, + cryptos: binance.CRYPTO, + }, + { + code: 'binanceus', + display: 'Binance.us', + class: EXCHANGE, + cryptos: binanceus.CRYPTO, + }, { code: 'cex', display: 'CEX.IO', class: EXCHANGE, cryptos: cex.CRYPTO }, - { code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS, dev: true }, - { code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS }, - { code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true }, + { + code: 'mock-wallet', + display: 'Mock (Caution!)', + class: WALLET, + cryptos: ALL_CRYPTOS, + dev: true, + }, + { + code: 'no-exchange', + display: 'No exchange', + class: EXCHANGE, + cryptos: ALL_CRYPTOS, + }, + { + code: 'mock-exchange', + display: 'Mock exchange', + class: EXCHANGE, + cryptos: ALL_CRYPTOS, + dev: true, + }, { code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true }, - { code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true }, + { + code: 'mock-id-verify', + display: 'Mock ID verifier', + class: ID_VERIFIER, + dev: true, + }, { code: 'twilio', display: 'Twilio', class: SMS }, { code: 'telnyx', display: 'Telnyx', class: SMS }, { code: 'vonage', display: 'Vonage', class: SMS }, @@ -58,17 +163,51 @@ const ALL_ACCOUNTS = [ { code: 'mailgun', display: 'Mailgun', class: EMAIL }, { code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true }, { code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS }, - { code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] }, - { code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true }, - { code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX] }, - { code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC] }, - { code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true }, + { + code: 'blockcypher', + display: 'Blockcypher', + class: ZERO_CONF, + cryptos: [BTC], + }, + { + code: 'mock-zero-conf', + display: 'Mock 0-conf', + class: ZERO_CONF, + cryptos: ALL_CRYPTOS, + dev: true, + }, + { + code: 'scorechain', + display: 'Scorechain', + class: WALLET_SCORING, + cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX], + }, + { + code: 'elliptic', + display: 'Elliptic', + class: WALLET_SCORING, + cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC], + }, + { + code: 'mock-scoring', + display: 'Mock scoring', + class: WALLET_SCORING, + cryptos: ALL_CRYPTOS, + dev: true, + }, { code: 'sumsub', display: 'Sumsub', class: COMPLIANCE }, - { code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true }, + { + code: 'mock-compliance', + display: 'Mock Compliance', + class: COMPLIANCE, + dev: true, + }, ] const flags = require('minimist')(process.argv.slice(2)) const devMode = flags.dev || flags.lamassuDev -const ACCOUNT_LIST = devMode ? ALL_ACCOUNTS : _.filter(it => !it.dev)(ALL_ACCOUNTS) +const ACCOUNT_LIST = devMode + ? ALL_ACCOUNTS + : _.filter(it => !it.dev)(ALL_ACCOUNTS) module.exports = { ACCOUNT_LIST } diff --git a/packages/server/lib/new-admin/config/data/countries.json b/packages/server/lib/new-admin/config/data/countries.json index e4fa0c3d..3f51f18d 100644 --- a/packages/server/lib/new-admin/config/data/countries.json +++ b/packages/server/lib/new-admin/config/data/countries.json @@ -1 +1,250 @@ -[{"code":"US","display":"United States"},{"code":"GB","display":"United Kingdom"},{"code":"CA","display":"Canada"},{"code":"AU","display":"Australia"},{"code":"AW","display":"Aruba"},{"code":"AF","display":"Afghanistan"},{"code":"AO","display":"Angola"},{"code":"AI","display":"Anguilla"},{"code":"AX","display":"Åland Islands"},{"code":"AL","display":"Albania"},{"code":"AD","display":"Andorra"},{"code":"AE","display":"United Arab Emirates"},{"code":"AR","display":"Argentina"},{"code":"AM","display":"Armenia"},{"code":"AS","display":"American Samoa"},{"code":"AQ","display":"Antarctica"},{"code":"TF","display":"French Southern and Antarctic Lands"},{"code":"AG","display":"Antigua and Barbuda"},{"code":"AT","display":"Austria"},{"code":"AZ","display":"Azerbaijan"},{"code":"BI","display":"Burundi"},{"code":"BE","display":"Belgium"},{"code":"BJ","display":"Benin"},{"code":"BF","display":"Burkina Faso"},{"code":"BD","display":"Bangladesh"},{"code":"BG","display":"Bulgaria"},{"code":"BH","display":"Bahrain"},{"code":"BS","display":"Bahamas"},{"code":"BA","display":"Bosnia and Herzegovina"},{"code":"BL","display":"Saint Barthélemy"},{"code":"BY","display":"Belarus"},{"code":"BZ","display":"Belize"},{"code":"BM","display":"Bermuda"},{"code":"BO","display":"Bolivia"},{"code":"BR","display":"Brazil"},{"code":"BB","display":"Barbados"},{"code":"BN","display":"Brunei"},{"code":"BT","display":"Bhutan"},{"code":"BV","display":"Bouvet Island"},{"code":"BW","display":"Botswana"},{"code":"CF","display":"Central African Republic"},{"code":"CC","display":"Cocos (Keeling) Islands"},{"code":"CH","display":"Switzerland"},{"code":"CL","display":"Chile"},{"code":"CN","display":"China"},{"code":"CI","display":"Ivory Coast"},{"code":"CM","display":"Cameroon"},{"code":"CD","display":"DR Congo"},{"code":"CG","display":"Republic of the Congo"},{"code":"CK","display":"Cook Islands"},{"code":"CO","display":"Colombia"},{"code":"KM","display":"Comoros"},{"code":"CV","display":"Cape Verde"},{"code":"CR","display":"Costa Rica"},{"code":"CU","display":"Cuba"},{"code":"CW","display":"Curaçao"},{"code":"CX","display":"Christmas Island"},{"code":"KY","display":"Cayman Islands"},{"code":"CY","display":"Cyprus"},{"code":"CZ","display":"Czech Republic"},{"code":"DE","display":"Germany"},{"code":"DJ","display":"Djibouti"},{"code":"DM","display":"Dominica"},{"code":"DK","display":"Denmark"},{"code":"DO","display":"Dominican Republic"},{"code":"DZ","display":"Algeria"},{"code":"EC","display":"Ecuador"},{"code":"EG","display":"Egypt"},{"code":"ER","display":"Eritrea"},{"code":"EH","display":"Western Sahara"},{"code":"ES","display":"Spain"},{"code":"EE","display":"Estonia"},{"code":"ET","display":"Ethiopia"},{"code":"FI","display":"Finland"},{"code":"FJ","display":"Fiji"},{"code":"FK","display":"Falkland Islands"},{"code":"FR","display":"France"},{"code":"FO","display":"Faroe Islands"},{"code":"FM","display":"Micronesia"},{"code":"GA","display":"Gabon"},{"code":"GE","display":"Georgia"},{"code":"GG","display":"Guernsey"},{"code":"GH","display":"Ghana"},{"code":"GI","display":"Gibraltar"},{"code":"GN","display":"Guinea"},{"code":"GP","display":"Guadeloupe"},{"code":"GM","display":"Gambia"},{"code":"GW","display":"Guinea-Bissau"},{"code":"GQ","display":"Equatorial Guinea"},{"code":"GR","display":"Greece"},{"code":"GD","display":"Grenada"},{"code":"GL","display":"Greenland"},{"code":"GT","display":"Guatemala"},{"code":"GF","display":"French Guiana"},{"code":"GU","display":"Guam"},{"code":"GY","display":"Guyana"},{"code":"HK","display":"Hong Kong"},{"code":"HM","display":"Heard Island and McDonald Islands"},{"code":"HN","display":"Honduras"},{"code":"HR","display":"Croatia"},{"code":"HT","display":"Haiti"},{"code":"HU","display":"Hungary"},{"code":"ID","display":"Indonesia"},{"code":"IM","display":"Isle of Man"},{"code":"IN","display":"India"},{"code":"IO","display":"British Indian Ocean Territory"},{"code":"IE","display":"Ireland"},{"code":"IR","display":"Iran"},{"code":"IQ","display":"Iraq"},{"code":"IS","display":"Iceland"},{"code":"IL","display":"Israel"},{"code":"IT","display":"Italy"},{"code":"JM","display":"Jamaica"},{"code":"JE","display":"Jersey"},{"code":"JO","display":"Jordan"},{"code":"JP","display":"Japan"},{"code":"KZ","display":"Kazakhstan"},{"code":"KE","display":"Kenya"},{"code":"KG","display":"Kyrgyzstan"},{"code":"KH","display":"Cambodia"},{"code":"KI","display":"Kiribati"},{"code":"KN","display":"Saint Kitts and Nevis"},{"code":"KR","display":"South Korea"},{"code":"XK","display":"Kosovo"},{"code":"KW","display":"Kuwait"},{"code":"LA","display":"Laos"},{"code":"LB","display":"Lebanon"},{"code":"LR","display":"Liberia"},{"code":"LY","display":"Libya"},{"code":"LC","display":"Saint Lucia"},{"code":"LI","display":"Liechtenstein"},{"code":"LK","display":"Sri Lanka"},{"code":"LS","display":"Lesotho"},{"code":"LT","display":"Lithuania"},{"code":"LU","display":"Luxembourg"},{"code":"LV","display":"Latvia"},{"code":"MO","display":"Macau"},{"code":"MF","display":"Saint Martin"},{"code":"MA","display":"Morocco"},{"code":"MC","display":"Monaco"},{"code":"MD","display":"Moldova"},{"code":"MG","display":"Madagascar"},{"code":"MV","display":"Maldives"},{"code":"MX","display":"Mexico"},{"code":"MH","display":"Marshall Islands"},{"code":"MK","display":"Macedonia"},{"code":"ML","display":"Mali"},{"code":"MT","display":"Malta"},{"code":"MM","display":"Myanmar"},{"code":"ME","display":"Montenegro"},{"code":"MN","display":"Mongolia"},{"code":"MP","display":"Northern Mariana Islands"},{"code":"MZ","display":"Mozambique"},{"code":"MR","display":"Mauritania"},{"code":"MS","display":"Montserrat"},{"code":"MQ","display":"Martinique"},{"code":"MU","display":"Mauritius"},{"code":"MW","display":"Malawi"},{"code":"MY","display":"Malaysia"},{"code":"YT","display":"Mayotte"},{"code":"NA","display":"Namibia"},{"code":"NC","display":"New Caledonia"},{"code":"NE","display":"Niger"},{"code":"NF","display":"Norfolk Island"},{"code":"NG","display":"Nigeria"},{"code":"NI","display":"Nicaragua"},{"code":"NU","display":"Niue"},{"code":"NL","display":"Netherlands"},{"code":"NO","display":"Norway"},{"code":"NP","display":"Nepal"},{"code":"NR","display":"Nauru"},{"code":"NZ","display":"New Zealand"},{"code":"OM","display":"Oman"},{"code":"PK","display":"Pakistan"},{"code":"PA","display":"Panama"},{"code":"PN","display":"Pitcairn Islands"},{"code":"PE","display":"Peru"},{"code":"PH","display":"Philippines"},{"code":"PW","display":"Palau"},{"code":"PG","display":"Papua New Guinea"},{"code":"PL","display":"Poland"},{"code":"PR","display":"Puerto Rico"},{"code":"KP","display":"North Korea"},{"code":"PT","display":"Portugal"},{"code":"PY","display":"Paraguay"},{"code":"PS","display":"Palestine"},{"code":"PF","display":"French Polynesia"},{"code":"QA","display":"Qatar"},{"code":"RE","display":"Réunion"},{"code":"RO","display":"Romania"},{"code":"RU","display":"Russia"},{"code":"RW","display":"Rwanda"},{"code":"SA","display":"Saudi Arabia"},{"code":"SD","display":"Sudan"},{"code":"SN","display":"Senegal"},{"code":"SG","display":"Singapore"},{"code":"GS","display":"South Georgia"},{"code":"SJ","display":"Svalbard and Jan Mayen"},{"code":"SB","display":"Solomon Islands"},{"code":"SL","display":"Sierra Leone"},{"code":"SV","display":"El Salvador"},{"code":"SM","display":"San Marino"},{"code":"SO","display":"Somalia"},{"code":"PM","display":"Saint Pierre and Miquelon"},{"code":"RS","display":"Serbia"},{"code":"SS","display":"South Sudan"},{"code":"ST","display":"São Tomé and Príncipe"},{"code":"SR","display":"Suriname"},{"code":"SK","display":"Slovakia"},{"code":"SI","display":"Slovenia"},{"code":"SE","display":"Sweden"},{"code":"SZ","display":"Swaziland"},{"code":"SX","display":"Sint Maarten"},{"code":"SC","display":"Seychelles"},{"code":"SY","display":"Syria"},{"code":"TC","display":"Turks and Caicos Islands"},{"code":"TD","display":"Chad"},{"code":"TG","display":"Togo"},{"code":"TH","display":"Thailand"},{"code":"TJ","display":"Tajikistan"},{"code":"TK","display":"Tokelau"},{"code":"TM","display":"Turkmenistan"},{"code":"TL","display":"Timor-Leste"},{"code":"TO","display":"Tonga"},{"code":"TT","display":"Trinidad and Tobago"},{"code":"TN","display":"Tunisia"},{"code":"TR","display":"Turkey"},{"code":"TV","display":"Tuvalu"},{"code":"TW","display":"Taiwan"},{"code":"TZ","display":"Tanzania"},{"code":"UG","display":"Uganda"},{"code":"UA","display":"Ukraine"},{"code":"UM","display":"United States Minor Outlying Islands"},{"code":"UY","display":"Uruguay"},{"code":"UZ","display":"Uzbekistan"},{"code":"VA","display":"Vatican City"},{"code":"VC","display":"Saint Vincent and the Grenadines"},{"code":"VE","display":"Venezuela"},{"code":"VG","display":"British Virgin Islands"},{"code":"VI","display":"United States Virgin Islands"},{"code":"VN","display":"Vietnam"},{"code":"VU","display":"Vanuatu"},{"code":"WF","display":"Wallis and Futuna"},{"code":"WS","display":"Samoa"},{"code":"YE","display":"Yemen"},{"code":"ZA","display":"South Africa"},{"code":"ZM","display":"Zambia"},{"code":"ZW","display":"Zimbabwe"}] \ No newline at end of file +[ + { "code": "US", "display": "United States" }, + { "code": "GB", "display": "United Kingdom" }, + { "code": "CA", "display": "Canada" }, + { "code": "AU", "display": "Australia" }, + { "code": "AW", "display": "Aruba" }, + { "code": "AF", "display": "Afghanistan" }, + { "code": "AO", "display": "Angola" }, + { "code": "AI", "display": "Anguilla" }, + { "code": "AX", "display": "Åland Islands" }, + { "code": "AL", "display": "Albania" }, + { "code": "AD", "display": "Andorra" }, + { "code": "AE", "display": "United Arab Emirates" }, + { "code": "AR", "display": "Argentina" }, + { "code": "AM", "display": "Armenia" }, + { "code": "AS", "display": "American Samoa" }, + { "code": "AQ", "display": "Antarctica" }, + { "code": "TF", "display": "French Southern and Antarctic Lands" }, + { "code": "AG", "display": "Antigua and Barbuda" }, + { "code": "AT", "display": "Austria" }, + { "code": "AZ", "display": "Azerbaijan" }, + { "code": "BI", "display": "Burundi" }, + { "code": "BE", "display": "Belgium" }, + { "code": "BJ", "display": "Benin" }, + { "code": "BF", "display": "Burkina Faso" }, + { "code": "BD", "display": "Bangladesh" }, + { "code": "BG", "display": "Bulgaria" }, + { "code": "BH", "display": "Bahrain" }, + { "code": "BS", "display": "Bahamas" }, + { "code": "BA", "display": "Bosnia and Herzegovina" }, + { "code": "BL", "display": "Saint Barthélemy" }, + { "code": "BY", "display": "Belarus" }, + { "code": "BZ", "display": "Belize" }, + { "code": "BM", "display": "Bermuda" }, + { "code": "BO", "display": "Bolivia" }, + { "code": "BR", "display": "Brazil" }, + { "code": "BB", "display": "Barbados" }, + { "code": "BN", "display": "Brunei" }, + { "code": "BT", "display": "Bhutan" }, + { "code": "BV", "display": "Bouvet Island" }, + { "code": "BW", "display": "Botswana" }, + { "code": "CF", "display": "Central African Republic" }, + { "code": "CC", "display": "Cocos (Keeling) Islands" }, + { "code": "CH", "display": "Switzerland" }, + { "code": "CL", "display": "Chile" }, + { "code": "CN", "display": "China" }, + { "code": "CI", "display": "Ivory Coast" }, + { "code": "CM", "display": "Cameroon" }, + { "code": "CD", "display": "DR Congo" }, + { "code": "CG", "display": "Republic of the Congo" }, + { "code": "CK", "display": "Cook Islands" }, + { "code": "CO", "display": "Colombia" }, + { "code": "KM", "display": "Comoros" }, + { "code": "CV", "display": "Cape Verde" }, + { "code": "CR", "display": "Costa Rica" }, + { "code": "CU", "display": "Cuba" }, + { "code": "CW", "display": "Curaçao" }, + { "code": "CX", "display": "Christmas Island" }, + { "code": "KY", "display": "Cayman Islands" }, + { "code": "CY", "display": "Cyprus" }, + { "code": "CZ", "display": "Czech Republic" }, + { "code": "DE", "display": "Germany" }, + { "code": "DJ", "display": "Djibouti" }, + { "code": "DM", "display": "Dominica" }, + { "code": "DK", "display": "Denmark" }, + { "code": "DO", "display": "Dominican Republic" }, + { "code": "DZ", "display": "Algeria" }, + { "code": "EC", "display": "Ecuador" }, + { "code": "EG", "display": "Egypt" }, + { "code": "ER", "display": "Eritrea" }, + { "code": "EH", "display": "Western Sahara" }, + { "code": "ES", "display": "Spain" }, + { "code": "EE", "display": "Estonia" }, + { "code": "ET", "display": "Ethiopia" }, + { "code": "FI", "display": "Finland" }, + { "code": "FJ", "display": "Fiji" }, + { "code": "FK", "display": "Falkland Islands" }, + { "code": "FR", "display": "France" }, + { "code": "FO", "display": "Faroe Islands" }, + { "code": "FM", "display": "Micronesia" }, + { "code": "GA", "display": "Gabon" }, + { "code": "GE", "display": "Georgia" }, + { "code": "GG", "display": "Guernsey" }, + { "code": "GH", "display": "Ghana" }, + { "code": "GI", "display": "Gibraltar" }, + { "code": "GN", "display": "Guinea" }, + { "code": "GP", "display": "Guadeloupe" }, + { "code": "GM", "display": "Gambia" }, + { "code": "GW", "display": "Guinea-Bissau" }, + { "code": "GQ", "display": "Equatorial Guinea" }, + { "code": "GR", "display": "Greece" }, + { "code": "GD", "display": "Grenada" }, + { "code": "GL", "display": "Greenland" }, + { "code": "GT", "display": "Guatemala" }, + { "code": "GF", "display": "French Guiana" }, + { "code": "GU", "display": "Guam" }, + { "code": "GY", "display": "Guyana" }, + { "code": "HK", "display": "Hong Kong" }, + { "code": "HM", "display": "Heard Island and McDonald Islands" }, + { "code": "HN", "display": "Honduras" }, + { "code": "HR", "display": "Croatia" }, + { "code": "HT", "display": "Haiti" }, + { "code": "HU", "display": "Hungary" }, + { "code": "ID", "display": "Indonesia" }, + { "code": "IM", "display": "Isle of Man" }, + { "code": "IN", "display": "India" }, + { "code": "IO", "display": "British Indian Ocean Territory" }, + { "code": "IE", "display": "Ireland" }, + { "code": "IR", "display": "Iran" }, + { "code": "IQ", "display": "Iraq" }, + { "code": "IS", "display": "Iceland" }, + { "code": "IL", "display": "Israel" }, + { "code": "IT", "display": "Italy" }, + { "code": "JM", "display": "Jamaica" }, + { "code": "JE", "display": "Jersey" }, + { "code": "JO", "display": "Jordan" }, + { "code": "JP", "display": "Japan" }, + { "code": "KZ", "display": "Kazakhstan" }, + { "code": "KE", "display": "Kenya" }, + { "code": "KG", "display": "Kyrgyzstan" }, + { "code": "KH", "display": "Cambodia" }, + { "code": "KI", "display": "Kiribati" }, + { "code": "KN", "display": "Saint Kitts and Nevis" }, + { "code": "KR", "display": "South Korea" }, + { "code": "XK", "display": "Kosovo" }, + { "code": "KW", "display": "Kuwait" }, + { "code": "LA", "display": "Laos" }, + { "code": "LB", "display": "Lebanon" }, + { "code": "LR", "display": "Liberia" }, + { "code": "LY", "display": "Libya" }, + { "code": "LC", "display": "Saint Lucia" }, + { "code": "LI", "display": "Liechtenstein" }, + { "code": "LK", "display": "Sri Lanka" }, + { "code": "LS", "display": "Lesotho" }, + { "code": "LT", "display": "Lithuania" }, + { "code": "LU", "display": "Luxembourg" }, + { "code": "LV", "display": "Latvia" }, + { "code": "MO", "display": "Macau" }, + { "code": "MF", "display": "Saint Martin" }, + { "code": "MA", "display": "Morocco" }, + { "code": "MC", "display": "Monaco" }, + { "code": "MD", "display": "Moldova" }, + { "code": "MG", "display": "Madagascar" }, + { "code": "MV", "display": "Maldives" }, + { "code": "MX", "display": "Mexico" }, + { "code": "MH", "display": "Marshall Islands" }, + { "code": "MK", "display": "Macedonia" }, + { "code": "ML", "display": "Mali" }, + { "code": "MT", "display": "Malta" }, + { "code": "MM", "display": "Myanmar" }, + { "code": "ME", "display": "Montenegro" }, + { "code": "MN", "display": "Mongolia" }, + { "code": "MP", "display": "Northern Mariana Islands" }, + { "code": "MZ", "display": "Mozambique" }, + { "code": "MR", "display": "Mauritania" }, + { "code": "MS", "display": "Montserrat" }, + { "code": "MQ", "display": "Martinique" }, + { "code": "MU", "display": "Mauritius" }, + { "code": "MW", "display": "Malawi" }, + { "code": "MY", "display": "Malaysia" }, + { "code": "YT", "display": "Mayotte" }, + { "code": "NA", "display": "Namibia" }, + { "code": "NC", "display": "New Caledonia" }, + { "code": "NE", "display": "Niger" }, + { "code": "NF", "display": "Norfolk Island" }, + { "code": "NG", "display": "Nigeria" }, + { "code": "NI", "display": "Nicaragua" }, + { "code": "NU", "display": "Niue" }, + { "code": "NL", "display": "Netherlands" }, + { "code": "NO", "display": "Norway" }, + { "code": "NP", "display": "Nepal" }, + { "code": "NR", "display": "Nauru" }, + { "code": "NZ", "display": "New Zealand" }, + { "code": "OM", "display": "Oman" }, + { "code": "PK", "display": "Pakistan" }, + { "code": "PA", "display": "Panama" }, + { "code": "PN", "display": "Pitcairn Islands" }, + { "code": "PE", "display": "Peru" }, + { "code": "PH", "display": "Philippines" }, + { "code": "PW", "display": "Palau" }, + { "code": "PG", "display": "Papua New Guinea" }, + { "code": "PL", "display": "Poland" }, + { "code": "PR", "display": "Puerto Rico" }, + { "code": "KP", "display": "North Korea" }, + { "code": "PT", "display": "Portugal" }, + { "code": "PY", "display": "Paraguay" }, + { "code": "PS", "display": "Palestine" }, + { "code": "PF", "display": "French Polynesia" }, + { "code": "QA", "display": "Qatar" }, + { "code": "RE", "display": "Réunion" }, + { "code": "RO", "display": "Romania" }, + { "code": "RU", "display": "Russia" }, + { "code": "RW", "display": "Rwanda" }, + { "code": "SA", "display": "Saudi Arabia" }, + { "code": "SD", "display": "Sudan" }, + { "code": "SN", "display": "Senegal" }, + { "code": "SG", "display": "Singapore" }, + { "code": "GS", "display": "South Georgia" }, + { "code": "SJ", "display": "Svalbard and Jan Mayen" }, + { "code": "SB", "display": "Solomon Islands" }, + { "code": "SL", "display": "Sierra Leone" }, + { "code": "SV", "display": "El Salvador" }, + { "code": "SM", "display": "San Marino" }, + { "code": "SO", "display": "Somalia" }, + { "code": "PM", "display": "Saint Pierre and Miquelon" }, + { "code": "RS", "display": "Serbia" }, + { "code": "SS", "display": "South Sudan" }, + { "code": "ST", "display": "São Tomé and Príncipe" }, + { "code": "SR", "display": "Suriname" }, + { "code": "SK", "display": "Slovakia" }, + { "code": "SI", "display": "Slovenia" }, + { "code": "SE", "display": "Sweden" }, + { "code": "SZ", "display": "Swaziland" }, + { "code": "SX", "display": "Sint Maarten" }, + { "code": "SC", "display": "Seychelles" }, + { "code": "SY", "display": "Syria" }, + { "code": "TC", "display": "Turks and Caicos Islands" }, + { "code": "TD", "display": "Chad" }, + { "code": "TG", "display": "Togo" }, + { "code": "TH", "display": "Thailand" }, + { "code": "TJ", "display": "Tajikistan" }, + { "code": "TK", "display": "Tokelau" }, + { "code": "TM", "display": "Turkmenistan" }, + { "code": "TL", "display": "Timor-Leste" }, + { "code": "TO", "display": "Tonga" }, + { "code": "TT", "display": "Trinidad and Tobago" }, + { "code": "TN", "display": "Tunisia" }, + { "code": "TR", "display": "Turkey" }, + { "code": "TV", "display": "Tuvalu" }, + { "code": "TW", "display": "Taiwan" }, + { "code": "TZ", "display": "Tanzania" }, + { "code": "UG", "display": "Uganda" }, + { "code": "UA", "display": "Ukraine" }, + { "code": "UM", "display": "United States Minor Outlying Islands" }, + { "code": "UY", "display": "Uruguay" }, + { "code": "UZ", "display": "Uzbekistan" }, + { "code": "VA", "display": "Vatican City" }, + { "code": "VC", "display": "Saint Vincent and the Grenadines" }, + { "code": "VE", "display": "Venezuela" }, + { "code": "VG", "display": "British Virgin Islands" }, + { "code": "VI", "display": "United States Virgin Islands" }, + { "code": "VN", "display": "Vietnam" }, + { "code": "VU", "display": "Vanuatu" }, + { "code": "WF", "display": "Wallis and Futuna" }, + { "code": "WS", "display": "Samoa" }, + { "code": "YE", "display": "Yemen" }, + { "code": "ZA", "display": "South Africa" }, + { "code": "ZM", "display": "Zambia" }, + { "code": "ZW", "display": "Zimbabwe" } +] diff --git a/packages/server/lib/new-admin/config/data/languages.json b/packages/server/lib/new-admin/config/data/languages.json index b61a3758..d055c3b2 100644 --- a/packages/server/lib/new-admin/config/data/languages.json +++ b/packages/server/lib/new-admin/config/data/languages.json @@ -1,255 +1,266 @@ { -"attribute": {"name":0, "nativeName":1}, -"rtl": {"ar":1,"dv":1,"fa":1,"ha":1,"he":1,"ks":1,"ku":1,"ps":1,"ur":1,"yi":1}, -"lang": { -"aa":["Afar","Afar"], -"ab":["Abkhazian","Аҧсуа"], -"af":["Afrikaans","Afrikaans"], -"ak":["Akan","Akana"], -"am":["Amharic","አማርኛ"], -"an":["Aragonese","Aragonés"], -"ar":["Arabic","العربية"], -"as":["Assamese","অসমীয়া"], -"av":["Avar","Авар"], -"ay":["Aymara","Aymar"], -"az":["Azerbaijani","Azərbaycanca / آذربايجان"], -"ba":["Bashkir","Башҡорт"], -"be":["Belarusian","Беларуская"], -"bg":["Bulgarian","Български"], -"bh":["Bihari","भोजपुरी"], -"bi":["Bislama","Bislama"], -"bm":["Bambara","Bamanankan"], -"bn":["Bengali","বাংলা"], -"bo":["Tibetan","བོད་ཡིག / Bod skad"], -"br":["Breton","Brezhoneg"], -"bs":["Bosnian","Bosanski"], -"ca":["Catalan","Català"], -"ce":["Chechen","Нохчийн"], -"ch":["Chamorro","Chamoru"], -"co":["Corsican","Corsu"], -"cr":["Cree","Nehiyaw"], -"cs":["Czech","Česky"], -"cu":["Old Church Slavonic / Old Bulgarian","словѣньскъ / slověnĭskŭ"], -"cv":["Chuvash","Чăваш"], -"cy":["Welsh","Cymraeg"], -"da":["Danish","Dansk"], -"de":["German","Deutsch"], -"dv":["Divehi","ދިވެހިބަސް"], -"dz":["Dzongkha","ཇོང་ཁ"], -"ee":["Ewe","Ɛʋɛ"], -"el":["Greek","Ελληνικά"], -"en":["English","English"], -"eo":["Esperanto","Esperanto"], -"es":["Spanish","Español"], -"et":["Estonian","Eesti"], -"eu":["Basque","Euskara"], -"fa":["Persian","فارسی"], -"ff":["Peul","Fulfulde"], -"fi":["Finnish","Suomi"], -"fj":["Fijian","Na Vosa Vakaviti"], -"fo":["Faroese","Føroyskt"], -"fr":["French","Français"], -"fy":["West Frisian","Frysk"], -"ga":["Irish","Gaeilge"], -"gd":["Scottish Gaelic","Gàidhlig"], -"gl":["Galician","Galego"], -"gn":["Guarani","Avañe'ẽ"], -"gu":["Gujarati","ગુજરાતી"], -"gv":["Manx","Gaelg"], -"ha":["Hausa","هَوُسَ"], -"he":["Hebrew","עברית"], -"hi":["Hindi","हिन्दी"], -"ho":["Hiri Motu","Hiri Motu"], -"hr":["Croatian","Hrvatski"], -"ht":["Haitian","Krèyol ayisyen"], -"hu":["Hungarian","Magyar"], -"hy":["Armenian","Հայերեն"], -"hz":["Herero","Otsiherero"], -"ia":["Interlingua","Interlingua"], -"id":["Indonesian","Bahasa Indonesia"], -"ie":["Interlingue","Interlingue"], -"ig":["Igbo","Igbo"], -"ii":["Sichuan Yi","ꆇꉙ / 四川彝语"], -"ik":["Inupiak","Iñupiak"], -"io":["Ido","Ido"], -"is":["Icelandic","Íslenska"], -"it":["Italian","Italiano"], -"iu":["Inuktitut","ᐃᓄᒃᑎᑐᑦ"], -"ja":["Japanese","日本語"], -"jv":["Javanese","Basa Jawa"], -"ka":["Georgian","ქართული"], -"kg":["Kongo","KiKongo"], -"ki":["Kikuyu","Gĩkũyũ"], -"kj":["Kuanyama","Kuanyama"], -"kk":["Kazakh","Қазақша"], -"kl":["Greenlandic","Kalaallisut"], -"km":["Cambodian","ភាសាខ្មែរ"], -"kn":["Kannada","ಕನ್ನಡ"], -"ko":["Korean","한국어"], -"kr":["Kanuri","Kanuri"], -"ks":["Kashmiri","कश्मीरी / كشميري"], -"ku":["Kurdish","Kurdî / كوردی"], -"kv":["Komi","Коми"], -"kw":["Cornish","Kernewek"], -"ky":["Kirghiz","Kırgızca / Кыргызча"], -"la":["Latin","Latina"], -"lb":["Luxembourgish","Lëtzebuergesch"], -"lg":["Ganda","Luganda"], -"li":["Limburgian","Limburgs"], -"ln":["Lingala","Lingála"], -"lo":["Laotian","ລາວ / Pha xa lao"], -"lt":["Lithuanian","Lietuvių"], -"lv":["Latvian","Latviešu"], -"mg":["Malagasy","Malagasy"], -"mh":["Marshallese","Kajin Majel / Ebon"], -"mi":["Maori","Māori"], -"mk":["Macedonian","Македонски"], -"ml":["Malayalam","മലയാളം"], -"mn":["Mongolian","Монгол"], -"mo":["Moldovan","Moldovenească"], -"mr":["Marathi","मराठी"], -"ms":["Malay","Bahasa Melayu"], -"mt":["Maltese","bil-Malti"], -"my":["Burmese","Myanmasa"], -"na":["Nauruan","Dorerin Naoero"], -"nd":["North Ndebele","Sindebele"], -"ne":["Nepali","नेपाली"], -"ng":["Ndonga","Oshiwambo"], -"nl":["Dutch","Nederlands"], -"nn":["Norwegian Nynorsk","Norsk (nynorsk)"], -"no":["Norwegian","Norsk (bokmål / riksmål)"], -"nr":["South Ndebele","isiNdebele"], -"nv":["Navajo","Diné bizaad"], -"ny":["Chichewa","Chi-Chewa"], -"oc":["Occitan","Occitan"], -"oj":["Ojibwa","ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"], -"om":["Oromo","Oromoo"], -"or":["Oriya","ଓଡ଼ିଆ"], -"os":["Ossetian / Ossetic","Иронау"], -"pa":["Panjabi / Punjabi","ਪੰਜਾਬੀ / पंजाबी / پنجابي"], -"pi":["Pali","Pāli / पाऴि"], -"pl":["Polish","Polski"], -"ps":["Pashto","پښتو"], -"pt":["Portuguese","Português"], -"qu":["Quechua","Runa Simi"], -"rm":["Raeto Romance","Rumantsch"], -"rn":["Kirundi","Kirundi"], -"ro":["Romanian","Română"], -"ru":["Russian","Русский"], -"rw":["Rwandi","Kinyarwandi"], -"sa":["Sanskrit","संस्कृतम्"], -"sc":["Sardinian","Sardu"], -"sd":["Sindhi","सिनधि"], -"se":["Northern Sami","Sámegiella"], -"sg":["Sango","Sängö"], -"sh":["Serbo-Croatian","Srpskohrvatski / Српскохрватски"], -"si":["Sinhalese","සිංහල"], -"sk":["Slovak","Slovenčina"], -"sl":["Slovenian","Slovenščina"], -"sm":["Samoan","Gagana Samoa"], -"sn":["Shona","chiShona"], -"so":["Somalia","Soomaaliga"], -"sq":["Albanian","Shqip"], -"sr":["Serbian","Српски"], -"ss":["Swati","SiSwati"], -"st":["Southern Sotho","Sesotho"], -"su":["Sundanese","Basa Sunda"], -"sv":["Swedish","Svenska"], -"sw":["Swahili","Kiswahili"], -"ta":["Tamil","தமிழ்"], -"te":["Telugu","తెలుగు"], -"tg":["Tajik","Тоҷикӣ"], -"th":["Thai","ไทย / Phasa Thai"], -"ti":["Tigrinya","ትግርኛ"], -"tk":["Turkmen","Туркмен / تركمن"], -"tl":["Tagalog / Filipino","Tagalog"], -"tn":["Tswana","Setswana"], -"to":["Tonga","Lea Faka-Tonga"], -"tr":["Turkish","Türkçe"], -"ts":["Tsonga","Xitsonga"], -"tt":["Tatar","Tatarça"], -"tw":["Twi","Twi"], -"ty":["Tahitian","Reo Mā`ohi"], -"ug":["Uyghur","Uyƣurqə / ئۇيغۇرچە"], -"uk":["Ukrainian","Українська"], -"ur":["Urdu","اردو"], -"uz":["Uzbek","Ўзбек"], -"ve":["Venda","Tshivenḓa"], -"vi":["Vietnamese","Tiếng Việt"], -"vo":["Volapük","Volapük"], -"wa":["Walloon","Walon"], -"wo":["Wolof","Wollof"], -"xh":["Xhosa","isiXhosa"], -"yi":["Yiddish","ייִדיש"], -"yo":["Yoruba","Yorùbá"], -"za":["Zhuang","Cuengh / Tôô / 壮语"], -"zh":["Chinese","中文"], -"zu":["Zulu","isiZulu"] -}, -"supported": [ - "en-US", - "en-CA", - "fr-QC", - "ach-UG", - "af-ZA", - "ar-SA", - "bg-BG", - "ca-ES", - "cs-CZ", - "cy-GB", - "de-DE", - "de-AT", - "de-CH", - "da-DK", - "el-GR", - "en-GB", - "en-AU", - "en-HK", - "en-IE", - "en-NZ", - "en-PR", - "es-ES", - "es-MX", - "et-EE", - "fi-FI", - "fr-FR", - "fr-CH", - "fur-IT", - "ga-IE", - "gd-GB", - "he-IL", - "hr-HR", - "hu-HU", - "hy-AM", - "id-ID", - "it-CH", - "it-IT", - "ja-JP", - "ka-GE", - "ko-KR", - "ky-KG", - "lt-LT", - "nb-NO", - "nl-BE", - "nl-NL", - "pt-PT", - "pt-BR", - "pl-PL", - "ro-RO", - "ru-RU", - "sco-GB", - "sh-HR", - "sk-SK", - "sl-SI", - "sr-SP", - "sv-SE", - "th-TH", - "tr-TR", - "uk-UA", - "vi-VN", - "zh-CN", - "zh-HK", - "zh-SG", - "zh-TW" -] + "attribute": { "name": 0, "nativeName": 1 }, + "rtl": { + "ar": 1, + "dv": 1, + "fa": 1, + "ha": 1, + "he": 1, + "ks": 1, + "ku": 1, + "ps": 1, + "ur": 1, + "yi": 1 + }, + "lang": { + "aa": ["Afar", "Afar"], + "ab": ["Abkhazian", "Аҧсуа"], + "af": ["Afrikaans", "Afrikaans"], + "ak": ["Akan", "Akana"], + "am": ["Amharic", "አማርኛ"], + "an": ["Aragonese", "Aragonés"], + "ar": ["Arabic", "العربية"], + "as": ["Assamese", "অসমীয়া"], + "av": ["Avar", "Авар"], + "ay": ["Aymara", "Aymar"], + "az": ["Azerbaijani", "Azərbaycanca / آذربايجان"], + "ba": ["Bashkir", "Башҡорт"], + "be": ["Belarusian", "Беларуская"], + "bg": ["Bulgarian", "Български"], + "bh": ["Bihari", "भोजपुरी"], + "bi": ["Bislama", "Bislama"], + "bm": ["Bambara", "Bamanankan"], + "bn": ["Bengali", "বাংলা"], + "bo": ["Tibetan", "བོད་ཡིག / Bod skad"], + "br": ["Breton", "Brezhoneg"], + "bs": ["Bosnian", "Bosanski"], + "ca": ["Catalan", "Català"], + "ce": ["Chechen", "Нохчийн"], + "ch": ["Chamorro", "Chamoru"], + "co": ["Corsican", "Corsu"], + "cr": ["Cree", "Nehiyaw"], + "cs": ["Czech", "Česky"], + "cu": ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"], + "cv": ["Chuvash", "Чăваш"], + "cy": ["Welsh", "Cymraeg"], + "da": ["Danish", "Dansk"], + "de": ["German", "Deutsch"], + "dv": ["Divehi", "ދިވެހިބަސް"], + "dz": ["Dzongkha", "ཇོང་ཁ"], + "ee": ["Ewe", "Ɛʋɛ"], + "el": ["Greek", "Ελληνικά"], + "en": ["English", "English"], + "eo": ["Esperanto", "Esperanto"], + "es": ["Spanish", "Español"], + "et": ["Estonian", "Eesti"], + "eu": ["Basque", "Euskara"], + "fa": ["Persian", "فارسی"], + "ff": ["Peul", "Fulfulde"], + "fi": ["Finnish", "Suomi"], + "fj": ["Fijian", "Na Vosa Vakaviti"], + "fo": ["Faroese", "Føroyskt"], + "fr": ["French", "Français"], + "fy": ["West Frisian", "Frysk"], + "ga": ["Irish", "Gaeilge"], + "gd": ["Scottish Gaelic", "Gàidhlig"], + "gl": ["Galician", "Galego"], + "gn": ["Guarani", "Avañe'ẽ"], + "gu": ["Gujarati", "ગુજરાતી"], + "gv": ["Manx", "Gaelg"], + "ha": ["Hausa", "هَوُسَ"], + "he": ["Hebrew", "עברית"], + "hi": ["Hindi", "हिन्दी"], + "ho": ["Hiri Motu", "Hiri Motu"], + "hr": ["Croatian", "Hrvatski"], + "ht": ["Haitian", "Krèyol ayisyen"], + "hu": ["Hungarian", "Magyar"], + "hy": ["Armenian", "Հայերեն"], + "hz": ["Herero", "Otsiherero"], + "ia": ["Interlingua", "Interlingua"], + "id": ["Indonesian", "Bahasa Indonesia"], + "ie": ["Interlingue", "Interlingue"], + "ig": ["Igbo", "Igbo"], + "ii": ["Sichuan Yi", "ꆇꉙ / 四川彝语"], + "ik": ["Inupiak", "Iñupiak"], + "io": ["Ido", "Ido"], + "is": ["Icelandic", "Íslenska"], + "it": ["Italian", "Italiano"], + "iu": ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"], + "ja": ["Japanese", "日本語"], + "jv": ["Javanese", "Basa Jawa"], + "ka": ["Georgian", "ქართული"], + "kg": ["Kongo", "KiKongo"], + "ki": ["Kikuyu", "Gĩkũyũ"], + "kj": ["Kuanyama", "Kuanyama"], + "kk": ["Kazakh", "Қазақша"], + "kl": ["Greenlandic", "Kalaallisut"], + "km": ["Cambodian", "ភាសាខ្មែរ"], + "kn": ["Kannada", "ಕನ್ನಡ"], + "ko": ["Korean", "한국어"], + "kr": ["Kanuri", "Kanuri"], + "ks": ["Kashmiri", "कश्मीरी / كشميري"], + "ku": ["Kurdish", "Kurdî / كوردی"], + "kv": ["Komi", "Коми"], + "kw": ["Cornish", "Kernewek"], + "ky": ["Kirghiz", "Kırgızca / Кыргызча"], + "la": ["Latin", "Latina"], + "lb": ["Luxembourgish", "Lëtzebuergesch"], + "lg": ["Ganda", "Luganda"], + "li": ["Limburgian", "Limburgs"], + "ln": ["Lingala", "Lingála"], + "lo": ["Laotian", "ລາວ / Pha xa lao"], + "lt": ["Lithuanian", "Lietuvių"], + "lv": ["Latvian", "Latviešu"], + "mg": ["Malagasy", "Malagasy"], + "mh": ["Marshallese", "Kajin Majel / Ebon"], + "mi": ["Maori", "Māori"], + "mk": ["Macedonian", "Македонски"], + "ml": ["Malayalam", "മലയാളം"], + "mn": ["Mongolian", "Монгол"], + "mo": ["Moldovan", "Moldovenească"], + "mr": ["Marathi", "मराठी"], + "ms": ["Malay", "Bahasa Melayu"], + "mt": ["Maltese", "bil-Malti"], + "my": ["Burmese", "Myanmasa"], + "na": ["Nauruan", "Dorerin Naoero"], + "nd": ["North Ndebele", "Sindebele"], + "ne": ["Nepali", "नेपाली"], + "ng": ["Ndonga", "Oshiwambo"], + "nl": ["Dutch", "Nederlands"], + "nn": ["Norwegian Nynorsk", "Norsk (nynorsk)"], + "no": ["Norwegian", "Norsk (bokmål / riksmål)"], + "nr": ["South Ndebele", "isiNdebele"], + "nv": ["Navajo", "Diné bizaad"], + "ny": ["Chichewa", "Chi-Chewa"], + "oc": ["Occitan", "Occitan"], + "oj": ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"], + "om": ["Oromo", "Oromoo"], + "or": ["Oriya", "ଓଡ଼ିଆ"], + "os": ["Ossetian / Ossetic", "Иронау"], + "pa": ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"], + "pi": ["Pali", "Pāli / पाऴि"], + "pl": ["Polish", "Polski"], + "ps": ["Pashto", "پښتو"], + "pt": ["Portuguese", "Português"], + "qu": ["Quechua", "Runa Simi"], + "rm": ["Raeto Romance", "Rumantsch"], + "rn": ["Kirundi", "Kirundi"], + "ro": ["Romanian", "Română"], + "ru": ["Russian", "Русский"], + "rw": ["Rwandi", "Kinyarwandi"], + "sa": ["Sanskrit", "संस्कृतम्"], + "sc": ["Sardinian", "Sardu"], + "sd": ["Sindhi", "सिनधि"], + "se": ["Northern Sami", "Sámegiella"], + "sg": ["Sango", "Sängö"], + "sh": ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"], + "si": ["Sinhalese", "සිංහල"], + "sk": ["Slovak", "Slovenčina"], + "sl": ["Slovenian", "Slovenščina"], + "sm": ["Samoan", "Gagana Samoa"], + "sn": ["Shona", "chiShona"], + "so": ["Somalia", "Soomaaliga"], + "sq": ["Albanian", "Shqip"], + "sr": ["Serbian", "Српски"], + "ss": ["Swati", "SiSwati"], + "st": ["Southern Sotho", "Sesotho"], + "su": ["Sundanese", "Basa Sunda"], + "sv": ["Swedish", "Svenska"], + "sw": ["Swahili", "Kiswahili"], + "ta": ["Tamil", "தமிழ்"], + "te": ["Telugu", "తెలుగు"], + "tg": ["Tajik", "Тоҷикӣ"], + "th": ["Thai", "ไทย / Phasa Thai"], + "ti": ["Tigrinya", "ትግርኛ"], + "tk": ["Turkmen", "Туркмен / تركمن"], + "tl": ["Tagalog / Filipino", "Tagalog"], + "tn": ["Tswana", "Setswana"], + "to": ["Tonga", "Lea Faka-Tonga"], + "tr": ["Turkish", "Türkçe"], + "ts": ["Tsonga", "Xitsonga"], + "tt": ["Tatar", "Tatarça"], + "tw": ["Twi", "Twi"], + "ty": ["Tahitian", "Reo Mā`ohi"], + "ug": ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"], + "uk": ["Ukrainian", "Українська"], + "ur": ["Urdu", "اردو"], + "uz": ["Uzbek", "Ўзбек"], + "ve": ["Venda", "Tshivenḓa"], + "vi": ["Vietnamese", "Tiếng Việt"], + "vo": ["Volapük", "Volapük"], + "wa": ["Walloon", "Walon"], + "wo": ["Wolof", "Wollof"], + "xh": ["Xhosa", "isiXhosa"], + "yi": ["Yiddish", "ייִדיש"], + "yo": ["Yoruba", "Yorùbá"], + "za": ["Zhuang", "Cuengh / Tôô / 壮语"], + "zh": ["Chinese", "中文"], + "zu": ["Zulu", "isiZulu"] + }, + "supported": [ + "en-US", + "en-CA", + "fr-QC", + "ach-UG", + "af-ZA", + "ar-SA", + "bg-BG", + "ca-ES", + "cs-CZ", + "cy-GB", + "de-DE", + "de-AT", + "de-CH", + "da-DK", + "el-GR", + "en-GB", + "en-AU", + "en-HK", + "en-IE", + "en-NZ", + "en-PR", + "es-ES", + "es-MX", + "et-EE", + "fi-FI", + "fr-FR", + "fr-CH", + "fur-IT", + "ga-IE", + "gd-GB", + "he-IL", + "hr-HR", + "hu-HU", + "hy-AM", + "id-ID", + "it-CH", + "it-IT", + "ja-JP", + "ka-GE", + "ko-KR", + "ky-KG", + "lt-LT", + "nb-NO", + "nl-BE", + "nl-NL", + "pt-PT", + "pt-BR", + "pl-PL", + "ro-RO", + "ru-RU", + "sco-GB", + "sh-HR", + "sk-SK", + "sl-SI", + "sr-SP", + "sv-SE", + "th-TH", + "tr-TR", + "uk-UA", + "vi-VN", + "zh-CN", + "zh-HK", + "zh-SG", + "zh-TW" + ] } diff --git a/packages/server/lib/new-admin/config/index.js b/packages/server/lib/new-admin/config/index.js index 0cabd55f..87fc75cf 100644 --- a/packages/server/lib/new-admin/config/index.js +++ b/packages/server/lib/new-admin/config/index.js @@ -7,10 +7,10 @@ const countries = require('./data/countries.json') const currenciesRec = require('./data/currencies.json') const languageRec = require('./data/languages.json') -function massageCurrencies (currencies) { +function massageCurrencies(currencies) { const convert = r => ({ code: r['Alphabetic Code'], - display: r['Currency'] + display: r['Currency'], }) const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD'] const mapped = _.map(convert, currencies) @@ -37,7 +37,7 @@ const massageCryptos = cryptos => { code: crypto['cryptoCode'], display: crypto['display'], codeDisplay: crypto['cryptoCodeDisplay'] ?? crypto['cryptoCode'], - isBeta: betaList.includes(crypto.cryptoCode) + isBeta: betaList.includes(crypto.cryptoCode), }) return _.map(convert, cryptos) diff --git a/packages/server/lib/new-admin/filters.js b/packages/server/lib/new-admin/filters.js index 4eb5dda7..8e152fe9 100644 --- a/packages/server/lib/new-admin/filters.js +++ b/packages/server/lib/new-admin/filters.js @@ -2,7 +2,7 @@ const db = require('../db') const cashInTx = require('../cash-in/cash-in-tx') const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper') -function transaction () { +function transaction() { const sql = `SELECT DISTINCT * FROM ( SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION @@ -27,7 +27,7 @@ function transaction () { return db.any(sql) } -function customer () { +function customer() { const sql = `SELECT DISTINCT * FROM ( SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION diff --git a/packages/server/lib/new-admin/graphql/directives/auth.js b/packages/server/lib/new-admin/graphql/directives/auth.js index 637da16b..24725613 100644 --- a/packages/server/lib/new-admin/graphql/directives/auth.js +++ b/packages/server/lib/new-admin/graphql/directives/auth.js @@ -7,7 +7,7 @@ const { AuthenticationError } = require('../errors') function authDirectiveTransformer(schema, directiveName = 'auth') { return mapSchema(schema, { // For object types - [MapperKind.OBJECT_TYPE]: (objectType) => { + [MapperKind.OBJECT_TYPE]: objectType => { const directive = getDirective(schema, objectType, directiveName)?.[0] if (directive) { const requiredAuthRole = directive.requires @@ -15,7 +15,7 @@ function authDirectiveTransformer(schema, directiveName = 'auth') { } return objectType }, - + // For field definitions [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => { const directive = getDirective(schema, fieldConfig, directiveName)?.[0] @@ -23,26 +23,30 @@ function authDirectiveTransformer(schema, directiveName = 'auth') { const requiredAuthRole = directive.requires fieldConfig._requiredAuthRole = requiredAuthRole } - + // Get the parent object type const objectType = schema.getType(typeName) - + // Apply auth check to the field's resolver const { resolve = defaultFieldResolver } = fieldConfig fieldConfig.resolve = function (root, args, context, info) { - const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole - if (!requiredRoles) return resolve.apply(this, [root, args, context, info]) - + const requiredRoles = + fieldConfig._requiredAuthRole || objectType._requiredAuthRole + if (!requiredRoles) + return resolve.apply(this, [root, args, context, info]) + const user = context.req.session.user if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) { - throw new AuthenticationError('You do not have permission to access this resource!') + throw new AuthenticationError( + 'You do not have permission to access this resource!', + ) } - + return resolve.apply(this, [root, args, context, info]) } - + return fieldConfig - } + }, }) } diff --git a/packages/server/lib/new-admin/graphql/errors.js b/packages/server/lib/new-admin/graphql/errors.js index 43d349c8..74206853 100644 --- a/packages/server/lib/new-admin/graphql/errors.js +++ b/packages/server/lib/new-admin/graphql/errors.js @@ -5,8 +5,8 @@ class AuthenticationError extends GraphQLError { constructor() { super('Authentication failed', { extensions: { - code: 'UNAUTHENTICATED' - } + code: 'UNAUTHENTICATED', + }, }) } } @@ -15,8 +15,8 @@ class InvalidCredentialsError extends GraphQLError { constructor() { super('Invalid credentials', { extensions: { - code: 'INVALID_CREDENTIALS' - } + code: 'INVALID_CREDENTIALS', + }, }) } } @@ -25,8 +25,8 @@ class UserAlreadyExistsError extends GraphQLError { constructor() { super('User already exists', { extensions: { - code: 'USER_ALREADY_EXISTS' - } + code: 'USER_ALREADY_EXISTS', + }, }) } } @@ -35,8 +35,8 @@ class InvalidTwoFactorError extends GraphQLError { constructor() { super('Invalid two-factor code', { extensions: { - code: 'INVALID_TWO_FACTOR_CODE' - } + code: 'INVALID_TWO_FACTOR_CODE', + }, }) } } @@ -45,8 +45,8 @@ class InvalidUrlError extends GraphQLError { constructor() { super('Invalid URL token', { extensions: { - code: 'INVALID_URL_TOKEN' - } + code: 'INVALID_URL_TOKEN', + }, }) } } @@ -55,8 +55,8 @@ class UserInputError extends GraphQLError { constructor() { super('User input error', { extensions: { - code: ApolloServerErrorCode.BAD_USER_INPUT - } + code: ApolloServerErrorCode.BAD_USER_INPUT, + }, }) } } @@ -67,5 +67,5 @@ module.exports = { UserAlreadyExistsError, InvalidTwoFactorError, InvalidUrlError, - UserInputError -} \ No newline at end of file + UserInputError, +} diff --git a/packages/server/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js b/packages/server/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js index b8ab9d74..4d3a087c 100644 --- a/packages/server/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js +++ b/packages/server/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js @@ -12,60 +12,70 @@ const devMode = require('minimist')(process.argv.slice(2)).dev const REMEMBER_ME_AGE = 90 * T.day const generateAttestationOptions = (session, options) => { - return users.getUserById(options.userId).then(user => { - return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user]) - }).then(([userDevices, user]) => { - const opts = simpleWebauthn.generateAttestationOptions({ - rpName: 'Lamassu', - rpID: options.domain, - userName: user.username, - userID: user.id, - timeout: 60000, - attestationType: 'indirect', - excludeCredentials: userDevices.map(dev => ({ - id: dev.data.credentialID, - type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] - })), - authenticatorSelection: { - userVerification: 'discouraged', - requireResidentKey: false - } + return users + .getUserById(options.userId) + .then(user => { + return Promise.all([ + credentials.getHardwareCredentialsByUserId(user.id), + user, + ]) }) - - session.webauthn = { - attestation: { - challenge: opts.challenge - } - } - - return opts - }) -} - -const generateAssertionOptions = (session, options) => { - return userManagement.authenticateUser(options.username, options.password).then(user => { - return credentials.getHardwareCredentialsByUserId(user.id).then(devices => { - const opts = simpleWebauthn.generateAssertionOptions({ + .then(([userDevices, user]) => { + const opts = simpleWebauthn.generateAttestationOptions({ + rpName: 'Lamassu', + rpID: options.domain, + userName: user.username, + userID: user.id, timeout: 60000, - allowCredentials: devices.map(dev => ({ + attestationType: 'indirect', + excludeCredentials: userDevices.map(dev => ({ id: dev.data.credentialID, type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] + transports: ['usb', 'ble', 'nfc', 'internal'], })), - userVerification: 'discouraged', - rpID: options.domain + authenticatorSelection: { + userVerification: 'discouraged', + requireResidentKey: false, + }, }) session.webauthn = { - assertion: { - challenge: opts.challenge - } + attestation: { + challenge: opts.challenge, + }, } return opts }) - }) +} + +const generateAssertionOptions = (session, options) => { + return userManagement + .authenticateUser(options.username, options.password) + .then(user => { + return credentials + .getHardwareCredentialsByUserId(user.id) + .then(devices => { + const opts = simpleWebauthn.generateAssertionOptions({ + timeout: 60000, + allowCredentials: devices.map(dev => ({ + id: dev.data.credentialID, + type: 'public-key', + transports: ['usb', 'ble', 'nfc', 'internal'], + })), + userVerification: 'discouraged', + rpID: options.domain, + }) + + session.webauthn = { + assertion: { + challenge: opts.challenge, + }, + } + + return opts + }) + }) } const validateAttestation = (session, options) => { @@ -78,98 +88,112 @@ const validateAttestation = (session, options) => { credential: options.attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, - expectedRPID: options.domain - }) - ]) - .then(([user, verification]) => { - const { verified, attestationInfo } = verification + expectedRPID: options.domain, + }), + ]).then(([user, verification]) => { + const { verified, attestationInfo } = verification - if (!(verified || attestationInfo)) { - session.webauthn = null - return false - } + if (!(verified || attestationInfo)) { + session.webauthn = null + return false + } - const { - counter, - credentialPublicKey, - credentialID - } = attestationInfo + const { counter, credentialPublicKey, credentialID } = attestationInfo - return credentials.getHardwareCredentialsByUserId(user.id) - .then(userDevices => { - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) + return credentials + .getHardwareCredentialsByUserId(user.id) + .then(userDevices => { + const existingDevice = userDevices.find( + device => device.data.credentialID === credentialID, + ) - if (!existingDevice) { - const newDevice = { - counter, - credentialPublicKey, - credentialID - } - credentials.createHardwareCredential(user.id, newDevice) + if (!existingDevice) { + const newDevice = { + counter, + credentialPublicKey, + credentialID, } + credentials.createHardwareCredential(user.id, newDevice) + } - session.webauthn = null - return verified - }) - }) + session.webauthn = null + return verified + }) + }) } const validateAssertion = (session, options) => { - return userManagement.authenticateUser(options.username, options.password).then(user => { - const expectedChallenge = session.webauthn.assertion.challenge + return userManagement + .authenticateUser(options.username, options.password) + .then(user => { + const expectedChallenge = session.webauthn.assertion.challenge - return credentials.getHardwareCredentialsByUserId(user.id).then(devices => { - const dbAuthenticator = _.find(dev => { - return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0 - }, devices) + return credentials + .getHardwareCredentialsByUserId(user.id) + .then(devices => { + const dbAuthenticator = _.find(dev => { + return ( + Buffer.from(dev.data.credentialID).compare( + base64url.toBuffer(options.assertionResponse.rawId), + ) === 0 + ) + }, devices) - if (!dbAuthenticator.data) { - throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`) - } + if (!dbAuthenticator.data) { + throw new Error( + `Could not find authenticator matching ${options.assertionResponse.id}`, + ) + } - const convertedAuthenticator = _.merge( - dbAuthenticator.data, - { credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) } - ) + const convertedAuthenticator = _.merge(dbAuthenticator.data, { + credentialPublicKey: Buffer.from( + dbAuthenticator.data.credentialPublicKey, + ), + }) - let verification - try { - verification = simpleWebauthn.verifyAssertionResponse({ - credential: options.assertionResponse, - expectedChallenge: `${expectedChallenge}`, - expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, - expectedRPID: options.domain, - authenticator: convertedAuthenticator - }) - } catch (err) { - console.error(err) - return false - } + let verification + try { + verification = simpleWebauthn.verifyAssertionResponse({ + credential: options.assertionResponse, + expectedChallenge: `${expectedChallenge}`, + expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, + expectedRPID: options.domain, + authenticator: convertedAuthenticator, + }) + } catch (err) { + console.error(err) + return false + } - const { verified, assertionInfo } = verification + const { verified, assertionInfo } = verification - if (!verified) { - session.webauthn = null - return false - } + if (!verified) { + session.webauthn = null + return false + } - dbAuthenticator.data.counter = assertionInfo.newCounter - return credentials.updateHardwareCredential(dbAuthenticator) - .then(() => { - const finalUser = { id: user.id, username: user.username, role: user.role } - session.user = finalUser - if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE + dbAuthenticator.data.counter = assertionInfo.newCounter + return credentials + .updateHardwareCredential(dbAuthenticator) + .then(() => { + const finalUser = { + id: user.id, + username: user.username, + role: user.role, + } + session.user = finalUser + if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE - session.webauthn = null - return verified + session.webauthn = null + return verified + }) }) }) - }) } module.exports = { generateAttestationOptions, generateAssertionOptions, validateAttestation, - validateAssertion + validateAssertion, } diff --git a/packages/server/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js b/packages/server/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js index 26091bde..1581df3b 100644 --- a/packages/server/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js +++ b/packages/server/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js @@ -11,35 +11,41 @@ const devMode = require('minimist')(process.argv.slice(2)).dev const REMEMBER_ME_AGE = 90 * T.day const generateAttestationOptions = (session, options) => { - return users.getUserById(options.userId).then(user => { - return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user]) - }).then(([userDevices, user]) => { - const opts = simpleWebauthn.generateAttestationOptions({ - rpName: 'Lamassu', - rpID: options.domain, - userName: user.username, - userID: user.id, - timeout: 60000, - attestationType: 'indirect', - excludeCredentials: userDevices.map(dev => ({ - id: dev.data.credentialID, - type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] - })), - authenticatorSelection: { - userVerification: 'discouraged', - requireResidentKey: false - } + return users + .getUserById(options.userId) + .then(user => { + return Promise.all([ + credentials.getHardwareCredentialsByUserId(user.id), + user, + ]) }) + .then(([userDevices, user]) => { + const opts = simpleWebauthn.generateAttestationOptions({ + rpName: 'Lamassu', + rpID: options.domain, + userName: user.username, + userID: user.id, + timeout: 60000, + attestationType: 'indirect', + excludeCredentials: userDevices.map(dev => ({ + id: dev.data.credentialID, + type: 'public-key', + transports: ['usb', 'ble', 'nfc', 'internal'], + })), + authenticatorSelection: { + userVerification: 'discouraged', + requireResidentKey: false, + }, + }) - session.webauthn = { - attestation: { - challenge: opts.challenge + session.webauthn = { + attestation: { + challenge: opts.challenge, + }, } - } - return opts - }) + return opts + }) } const generateAssertionOptions = (session, options) => { @@ -50,16 +56,16 @@ const generateAssertionOptions = (session, options) => { allowCredentials: devices.map(dev => ({ id: dev.data.credentialID, type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] + transports: ['usb', 'ble', 'nfc', 'internal'], })), userVerification: 'discouraged', - rpID: options.domain + rpID: options.domain, }) session.webauthn = { assertion: { - challenge: opts.challenge - } + challenge: opts.challenge, + }, } return opts @@ -77,40 +83,38 @@ const validateAttestation = (session, options) => { credential: options.attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, - expectedRPID: options.domain - }) - ]) - .then(([user, verification]) => { - const { verified, attestationInfo } = verification + expectedRPID: options.domain, + }), + ]).then(([user, verification]) => { + const { verified, attestationInfo } = verification - if (!(verified || attestationInfo)) { - session.webauthn = null - return false - } + if (!(verified || attestationInfo)) { + session.webauthn = null + return false + } - const { - counter, - credentialPublicKey, - credentialID - } = attestationInfo + const { counter, credentialPublicKey, credentialID } = attestationInfo - return credentials.getHardwareCredentialsByUserId(user.id) - .then(userDevices => { - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) + return credentials + .getHardwareCredentialsByUserId(user.id) + .then(userDevices => { + const existingDevice = userDevices.find( + device => device.data.credentialID === credentialID, + ) - if (!existingDevice) { - const newDevice = { - counter, - credentialPublicKey, - credentialID - } - credentials.createHardwareCredential(user.id, newDevice) + if (!existingDevice) { + const newDevice = { + counter, + credentialPublicKey, + credentialID, } + credentials.createHardwareCredential(user.id, newDevice) + } - session.webauthn = null - return verified - }) - }) + session.webauthn = null + return verified + }) + }) } const validateAssertion = (session, options) => { @@ -119,17 +123,24 @@ const validateAssertion = (session, options) => { return credentials.getHardwareCredentialsByUserId(user.id).then(devices => { const dbAuthenticator = _.find(dev => { - return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0 + return ( + Buffer.from(dev.data.credentialID).compare( + base64url.toBuffer(options.assertionResponse.rawId), + ) === 0 + ) }, devices) if (!dbAuthenticator.data) { - throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`) + throw new Error( + `Could not find authenticator matching ${options.assertionResponse.id}`, + ) } - const convertedAuthenticator = _.merge( - dbAuthenticator.data, - { credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) } - ) + const convertedAuthenticator = _.merge(dbAuthenticator.data, { + credentialPublicKey: Buffer.from( + dbAuthenticator.data.credentialPublicKey, + ), + }) let verification try { @@ -138,7 +149,7 @@ const validateAssertion = (session, options) => { expectedChallenge: `${expectedChallenge}`, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, expectedRPID: options.domain, - authenticator: convertedAuthenticator + authenticator: convertedAuthenticator, }) } catch (err) { console.error(err) @@ -148,20 +159,22 @@ const validateAssertion = (session, options) => { const { verified, assertionInfo } = verification if (!verified) { - context.req.session.webauthn = null return false } dbAuthenticator.data.counter = assertionInfo.newCounter - return credentials.updateHardwareCredential(dbAuthenticator) - .then(() => { - const finalUser = { id: user.id, username: user.username, role: user.role } - session.user = finalUser - if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE + return credentials.updateHardwareCredential(dbAuthenticator).then(() => { + const finalUser = { + id: user.id, + username: user.username, + role: user.role, + } + session.user = finalUser + if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE - session.webauthn = null - return verified - }) + session.webauthn = null + return verified + }) }) }) } @@ -170,5 +183,5 @@ module.exports = { generateAttestationOptions, generateAssertionOptions, validateAttestation, - validateAssertion + validateAssertion, } diff --git a/packages/server/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js b/packages/server/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js index b547d4f5..0d41f240 100644 --- a/packages/server/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js +++ b/packages/server/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js @@ -22,19 +22,19 @@ const generateAttestationOptions = (session, options) => { excludeCredentials: devices.map(dev => ({ id: dev.data.credentialID, type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] + transports: ['usb', 'ble', 'nfc', 'internal'], })), authenticatorSelection: { authenticatorAttachment: 'cross-platform', userVerification: 'discouraged', - requireResidentKey: false - } + requireResidentKey: false, + }, }) session.webauthn = { attestation: { - challenge: opts.challenge - } + challenge: opts.challenge, + }, } return opts @@ -48,16 +48,16 @@ const generateAssertionOptions = (session, options) => { allowCredentials: devices.map(dev => ({ id: dev.data.credentialID, type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] + transports: ['usb', 'ble', 'nfc', 'internal'], })), userVerification: 'discouraged', - rpID: options.domain + rpID: options.domain, }) session.webauthn = { assertion: { - challenge: opts.challenge - } + challenge: opts.challenge, + }, } return opts }) @@ -73,50 +73,52 @@ const validateAttestation = (session, options) => { credential: options.attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, - expectedRPID: options.domain - }) - ]) - .then(([user, verification]) => { - const { verified, attestationInfo } = verification + expectedRPID: options.domain, + }), + ]).then(([user, verification]) => { + const { verified, attestationInfo } = verification + + if (!(verified || attestationInfo)) { + session.webauthn = null + return verified + } + + const { + fmt, + counter, + aaguid, + credentialPublicKey, + credentialID, + credentialType, + userVerified, + attestationObject, + } = attestationInfo + + return credentials + .getHardwareCredentialsByUserId(user.id) + .then(userDevices => { + const existingDevice = userDevices.find( + device => device.data.credentialID === credentialID, + ) + + if (!existingDevice) { + const newDevice = { + fmt, + counter, + aaguid, + credentialPublicKey, + credentialID, + credentialType, + userVerified, + attestationObject, + } + credentials.createHardwareCredential(user.id, newDevice) + } - if (!(verified || attestationInfo)) { session.webauthn = null return verified - } - - const { - fmt, - counter, - aaguid, - credentialPublicKey, - credentialID, - credentialType, - userVerified, - attestationObject - } = attestationInfo - - return credentials.getHardwareCredentialsByUserId(user.id) - .then(userDevices => { - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) - - if (!existingDevice) { - const newDevice = { - fmt, - counter, - aaguid, - credentialPublicKey, - credentialID, - credentialType, - userVerified, - attestationObject - } - credentials.createHardwareCredential(user.id, newDevice) - } - - session.webauthn = null - return verified - }) - }) + }) + }) } const validateAssertion = (session, options) => { @@ -124,17 +126,24 @@ const validateAssertion = (session, options) => { return credentials.getHardwareCredentials().then(devices => { const dbAuthenticator = _.find(dev => { - return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0 + return ( + Buffer.from(dev.data.credentialID).compare( + base64url.toBuffer(options.assertionResponse.rawId), + ) === 0 + ) }, devices) if (!dbAuthenticator.data) { - throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`) + throw new Error( + `Could not find authenticator matching ${options.assertionResponse.id}`, + ) } - const convertedAuthenticator = _.merge( - dbAuthenticator.data, - { credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) } - ) + const convertedAuthenticator = _.merge(dbAuthenticator.data, { + credentialPublicKey: Buffer.from( + dbAuthenticator.data.credentialPublicKey, + ), + }) let verification try { @@ -143,7 +152,7 @@ const validateAssertion = (session, options) => { expectedChallenge: `${expectedChallenge}`, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`, expectedRPID: options.domain, - authenticator: convertedAuthenticator + authenticator: convertedAuthenticator, }) } catch (err) { console.error(err) @@ -160,16 +169,19 @@ const validateAssertion = (session, options) => { dbAuthenticator.data.counter = assertionInfo.newCounter return Promise.all([ credentials.updateHardwareCredential(dbAuthenticator), - users.getUserById(dbAuthenticator.user_id) - ]) - .then(([_, user]) => { - const finalUser = { id: user.id, username: user.username, role: user.role } - session.user = finalUser - session.cookie.maxAge = REMEMBER_ME_AGE - - session.webauthn = null - return verified - }) + users.getUserById(dbAuthenticator.user_id), + ]).then(([, user]) => { + const finalUser = { + id: user.id, + username: user.username, + role: user.role, + } + session.user = finalUser + session.cookie.maxAge = REMEMBER_ME_AGE + + session.webauthn = null + return verified + }) }) } @@ -177,5 +189,5 @@ module.exports = { generateAttestationOptions, generateAssertionOptions, validateAttestation, - validateAssertion + validateAssertion, } diff --git a/packages/server/lib/new-admin/graphql/modules/authentication/index.js b/packages/server/lib/new-admin/graphql/modules/authentication/index.js index 1b4591cc..924cf427 100644 --- a/packages/server/lib/new-admin/graphql/modules/authentication/index.js +++ b/packages/server/lib/new-admin/graphql/modules/authentication/index.js @@ -5,7 +5,7 @@ const FIDOUsernameless = require('./FIDOUsernamelessStrategy') const STRATEGIES = { FIDO2FA, FIDOPasswordless, - FIDOUsernameless + FIDOUsernameless, } // FIDO2FA, FIDOPasswordless or FIDOUsernameless @@ -13,5 +13,5 @@ const CHOSEN_STRATEGY = 'FIDO2FA' module.exports = { CHOSEN_STRATEGY, - strategy: STRATEGIES[CHOSEN_STRATEGY] + strategy: STRATEGIES[CHOSEN_STRATEGY], } diff --git a/packages/server/lib/new-admin/graphql/modules/userManagement.js b/packages/server/lib/new-admin/graphql/modules/userManagement.js index 320c8f4c..492afa97 100644 --- a/packages/server/lib/new-admin/graphql/modules/userManagement.js +++ b/packages/server/lib/new-admin/graphql/modules/userManagement.js @@ -14,11 +14,16 @@ const credentials = require('../../../hardware-credentials') const REMEMBER_ME_AGE = 90 * T.day const authenticateUser = (username, password) => { - return users.getUserByUsername(username) + return users + .getUserByUsername(username) .then(user => { const hashedPassword = user.password - if (!hashedPassword || !user.enabled) throw new authErrors.InvalidCredentialsError() - return Promise.all([argon2.verify(hashedPassword, password), hashedPassword]) + if (!hashedPassword || !user.enabled) + throw new authErrors.InvalidCredentialsError() + return Promise.all([ + argon2.verify(hashedPassword, password), + hashedPassword, + ]) }) .then(([isMatch, hashedPassword]) => { if (!isMatch) throw new authErrors.InvalidCredentialsError() @@ -32,7 +37,9 @@ const authenticateUser = (username, password) => { const destroySessionIfSameUser = (context, user) => { const sessionUser = getUserFromCookie(context) - if (sessionUser && user.id === sessionUser.id) { context.req.session.destroy() } + if (sessionUser && user.id === sessionUser.id) { + context.req.session.destroy() + } } const destroySessionIfBeingUsed = (sessID, context) => { @@ -56,15 +63,13 @@ const initializeSession = (context, user, rememberMe) => { } const executeProtectedAction = (code, id, context, action) => { - return users.getUserById(id) - .then(user => { - if (user.role !== 'superuser') { - return action() - } + return users.getUserById(id).then(user => { + if (user.role !== 'superuser') { + return action() + } - return confirm2FA(code, context) - .then(() => action()) - }) + return confirm2FA(code, context).then(() => action()) + }) } const getUserData = context => { @@ -79,10 +84,18 @@ const get2FASecret = (username, password) => { return authenticateUser(username, password) .then(user => { const secret = otplib.authenticator.generateSecret() - const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret) - return Promise.all([users.saveTemp2FASecret(user.id, secret), secret, otpauth]) + const otpauth = otplib.authenticator.keyuri( + user.username, + constants.AUTHENTICATOR_ISSUER_ENTITY, + secret, + ) + return Promise.all([ + users.saveTemp2FASecret(user.id, secret), + secret, + otpauth, + ]) }) - .then(([_, secret, otpauth]) => { + .then(([, secret, otpauth]) => { return { secret, otpauth } }) } @@ -103,35 +116,43 @@ const confirm2FA = (token, context) => { const validateRegisterLink = token => { if (!token) throw new authErrors.InvalidUrlError() - return users.validateUserRegistrationToken(token) - .then(r => { - if (!r.success) throw new authErrors.InvalidUrlError() - return { username: r.username, role: r.role } - }) + return users.validateUserRegistrationToken(token).then(r => { + if (!r.success) throw new authErrors.InvalidUrlError() + return { username: r.username, role: r.role } + }) } const validateResetPasswordLink = token => { if (!token) throw new authErrors.InvalidUrlError() - return users.validateAuthToken(token, 'reset_password') - .then(r => { - if (!r.success) throw new authErrors.InvalidUrlError() - return { id: r.userID } - }) + return users.validateAuthToken(token, 'reset_password').then(r => { + if (!r.success) throw new authErrors.InvalidUrlError() + return { id: r.userID } + }) } const validateReset2FALink = token => { if (!token) throw new authErrors.InvalidUrlError() - return users.validateAuthToken(token, 'reset_twofa') + return users + .validateAuthToken(token, 'reset_twofa') .then(r => { if (!r.success) throw new authErrors.InvalidUrlError() return users.getUserById(r.userID) }) .then(user => { const secret = otplib.authenticator.generateSecret() - const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret) - return Promise.all([users.saveTemp2FASecret(user.id, secret), user, secret, otpauth]) + const otpauth = otplib.authenticator.keyuri( + user.username, + constants.AUTHENTICATOR_ISSUER_ENTITY, + secret, + ) + return Promise.all([ + users.saveTemp2FASecret(user.id, secret), + user, + secret, + otpauth, + ]) }) - .then(([_, user, secret, otpauth]) => { + .then(([, user, secret, otpauth]) => { return { user_id: user.id, secret, otpauth } }) } @@ -144,7 +165,10 @@ const deleteSession = (sessionID, context) => { const login = (username, password) => { return authenticateUser(username, password) .then(user => { - return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user.twofa_code]) + return Promise.all([ + credentials.getHardwareCredentialsByUserId(user.id), + user.twofa_code, + ]) }) .then(([devices, twoFASecret]) => { if (!_.isEmpty(devices)) return 'FIDO' @@ -153,21 +177,32 @@ const login = (username, password) => { } const input2FA = (username, password, rememberMe, code, context) => { - return authenticateUser(username, password) - .then(user => { - const secret = user.twofa_code - const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret }) - if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() - - initializeSession(context, user, rememberMe) - return true + return authenticateUser(username, password).then(user => { + const secret = user.twofa_code + const isCodeValid = otplib.authenticator.verify({ + token: code, + secret: secret, }) + if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() + + initializeSession(context, user, rememberMe) + return true + }) } -const setup2FA = (username, password, rememberMe, codeConfirmation, context) => { +const setup2FA = ( + username, + password, + rememberMe, + codeConfirmation, + context, +) => { return authenticateUser(username, password) .then(user => { - const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: user.temp_twofa_code }) + const isCodeValid = otplib.authenticator.verify({ + token: codeConfirmation, + secret: user.temp_twofa_code, + }) if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() initializeSession(context, user, rememberMe) @@ -202,24 +237,23 @@ const createReset2FAToken = (code, userID, context) => { } const createRegisterToken = (username, role) => { - return users.getUserByUsername(username) - .then(user => { - if (user) throw new authErrors.UserAlreadyExistsError() + return users.getUserByUsername(username).then(user => { + if (user) throw new authErrors.UserAlreadyExistsError() - return users.createUserRegistrationToken(username, role) - }) + return users.createUserRegistrationToken(username, role) + }) } const register = (token, username, password, role) => { - return users.getUserByUsername(username) - .then(user => { - if (user) throw new authErrors.UserAlreadyExistsError() - return users.register(token, username, password, role).then(() => true) - }) + return users.getUserByUsername(username).then(user => { + if (user) throw new authErrors.UserAlreadyExistsError() + return users.register(token, username, password, role).then(() => true) + }) } const resetPassword = (token, userID, newPassword, context) => { - return users.getUserById(userID) + return users + .getUserById(userID) .then(user => { destroySessionIfSameUser(context, user) return users.updatePassword(token, user.id, newPassword) @@ -228,9 +262,13 @@ const resetPassword = (token, userID, newPassword, context) => { } const reset2FA = (token, userID, code, context) => { - return users.getUserById(userID) + return users + .getUserById(userID) .then(user => { - const isCodeValid = otplib.authenticator.verify({ token: code, secret: user.temp_twofa_code }) + const isCodeValid = otplib.authenticator.verify({ + token: code, + secret: user.temp_twofa_code, + }) if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() destroySessionIfSameUser(context, user) @@ -240,7 +278,10 @@ const reset2FA = (token, userID, code, context) => { } const getToken = context => { - if (_.isNil(context.req.cookies['lamassu_sid']) || _.isNil(context.req.session.user.id)) + if ( + _.isNil(context.req.cookies['lamassu_sid']) || + _.isNil(context.req.session.user.id) + ) throw new authErrors.AuthenticationError('Authentication failed') return context.req.session.user.id @@ -267,5 +308,5 @@ module.exports = { register, resetPassword, reset2FA, - getToken + getToken, } diff --git a/packages/server/lib/new-admin/graphql/resolvers/bill.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/bill.resolver.js index 7128332b..b899d517 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/bill.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/bill.resolver.js @@ -2,8 +2,8 @@ const bills = require('../../services/bills') const resolvers = { Query: { - bills: (...[, { filters }]) => bills.getBills(filters) - } + bills: (...[, { filters }]) => bills.getBills(filters), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/blacklist.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/blacklist.resolver.js index e0b63d53..6effa47e 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/blacklist.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/blacklist.resolver.js @@ -3,7 +3,7 @@ const blacklist = require('../../../blacklist') const resolvers = { Query: { blacklist: () => blacklist.getBlacklist(), - blacklistMessages: () => blacklist.getMessages() + blacklistMessages: () => blacklist.getMessages(), }, Mutation: { deleteBlacklistRow: (...[, { address }]) => @@ -11,8 +11,8 @@ const resolvers = { insertBlacklistRow: (...[, { address }]) => blacklist.insertIntoBlacklist(address), editBlacklistMessage: (...[, { id, content }]) => - blacklist.editBlacklistMessage(id, content) - } + blacklist.editBlacklistMessage(id, content), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/cashbox.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/cashbox.resolver.js index 193666c5..31c3005d 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/cashbox.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/cashbox.resolver.js @@ -5,13 +5,21 @@ const logDateFormat = require('../../../logs').logDateFormat const resolvers = { Query: { cashboxBatches: () => cashbox.getBatches(), - cashboxBatchesCsv: (...[, { from, until, timezone }]) => cashbox.getBatches(from, until) - .then(data => parseAsync(logDateFormat(timezone, cashbox.logFormatter(data), ['created']))) + cashboxBatchesCsv: (...[, { from, until, timezone }]) => + cashbox + .getBatches(from, until) + .then(data => + parseAsync( + logDateFormat(timezone, cashbox.logFormatter(data), ['created']), + ), + ), }, Mutation: { - createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount), - editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy) - } + createBatch: (...[, { deviceId, cashboxCount }]) => + cashbox.createCashboxBatch(deviceId, cashboxCount), + editBatch: (...[, { id, performedBy }]) => + cashbox.editBatchById(id, performedBy), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/config.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/config.resolver.js index 6bb38a22..e793541c 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/config.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/config.resolver.js @@ -1,11 +1,15 @@ -const { accounts: accountsConfig, countries, languages } = require('../../config') +const { + accounts: accountsConfig, + countries, + languages, +} = require('../../config') const resolver = { Query: { countries: () => countries, languages: () => languages, - accountsConfig: () => accountsConfig - } + accountsConfig: () => accountsConfig, + }, } module.exports = resolver diff --git a/packages/server/lib/new-admin/graphql/resolvers/currency.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/currency.resolver.js index dba21069..2574298b 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/currency.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/currency.resolver.js @@ -3,8 +3,8 @@ const { coins, currencies } = require('../../config') const resolver = { Query: { currencies: () => currencies, - cryptoCurrencies: () => coins - } + cryptoCurrencies: () => coins, + }, } module.exports = resolver diff --git a/packages/server/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js index fefdcf6b..0b0f99d3 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js @@ -2,32 +2,55 @@ const authentication = require('../modules/userManagement') const queries = require('../../services/customInfoRequests') const DataLoader = require('dataloader') -const customerCustomInfoRequestsLoader = new DataLoader(ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), { cache: false }) +const customerCustomInfoRequestsLoader = new DataLoader( + ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), + { cache: false }, +) -const customInfoRequestLoader = new DataLoader(ids => queries.batchGetCustomInfoRequest(ids), { cache: false }) +const customInfoRequestLoader = new DataLoader( + ids => queries.batchGetCustomInfoRequest(ids), + { cache: false }, +) const resolvers = { Customer: { - customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id) + customInfoRequests: parent => + customerCustomInfoRequestsLoader.load(parent.id), }, CustomRequestData: { - customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId) + customInfoRequest: parent => + customInfoRequestLoader.load(parent.infoRequestId), }, Query: { - customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled), - customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId), - customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId) + customInfoRequests: (...[, { onlyEnabled }]) => + queries.getCustomInfoRequests(onlyEnabled), + customerCustomInfoRequests: (...[, { customerId }]) => + queries.getAllCustomInfoRequestsForCustomer(customerId), + customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => + queries.getCustomInfoRequestForCustomer(customerId, infoRequestId), }, Mutation: { - insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest), - removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id), - editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest), - setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => { + insertCustomInfoRequest: (...[, { customRequest }]) => + queries.addCustomInfoRequest(customRequest), + removeCustomInfoRequest: (...[, { id }]) => + queries.removeCustomInfoRequest(id), + editCustomInfoRequest: (...[, { id, customRequest }]) => + queries.editCustomInfoRequest(id, customRequest), + setAuthorizedCustomRequest: ( + ...[, { customerId, infoRequestId, override }, context] + ) => { const token = authentication.getToken(context) - return queries.setAuthorizedCustomRequest(customerId, infoRequestId, override, token) + return queries.setAuthorizedCustomRequest( + customerId, + infoRequestId, + override, + token, + ) }, - setCustomerCustomInfoRequest: (...[, { customerId, infoRequestId, data }]) => queries.setCustomerData(customerId, infoRequestId, data) - } + setCustomerCustomInfoRequest: ( + ...[, { customerId, infoRequestId, data }] + ) => queries.setCustomerData(customerId, infoRequestId, data), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js index 30f5159c..bdd7c640 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -6,41 +6,56 @@ const customerNotes = require('../../../customer-notes') const machineLoader = require('../../../machine-loader') const addLastUsedMachineName = customer => - (customer.lastUsedMachine ? machineLoader.getMachineName(customer.lastUsedMachine) : Promise.resolve(null)) - .then(lastUsedMachineName => Object.assign(customer, { lastUsedMachineName })) + (customer.lastUsedMachine + ? machineLoader.getMachineName(customer.lastUsedMachine) + : Promise.resolve(null) + ).then(lastUsedMachineName => + Object.assign(customer, { lastUsedMachineName }), + ) const resolvers = { Customer: { - isAnonymous: parent => (parent.customerId === anonymous.uuid) + isAnonymous: parent => parent.customerId === anonymous.uuid, }, Query: { - customers: (...[, { phone, email, name, address, id }]) => customers.getCustomersList(phone, name, address, id, email), - customer: (...[, { customerId }]) => customers.getCustomerById(customerId).then(addLastUsedMachineName), - customerFilters: () => filters.customer() + customers: (...[, { phone, email, name, address, id }]) => + customers.getCustomersList(phone, name, address, id, email), + customer: (...[, { customerId }]) => + customers.getCustomerById(customerId).then(addLastUsedMachineName), + customerFilters: () => filters.customer(), }, Mutation: { - setCustomer: (root, { customerId, customerInput }, context, info) => { + setCustomer: (root, { customerId, customerInput }, context) => { const token = authentication.getToken(context) - if (customerId === anonymous.uuid) return customers.getCustomerById(customerId) + if (customerId === anonymous.uuid) + return customers.getCustomerById(customerId) return customers.updateCustomer(customerId, customerInput, token) }, - addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value), - saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value), - removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId), + addCustomField: (...[, { customerId, label, value }]) => + customers.addCustomField(customerId, label, value), + saveCustomField: (...[, { customerId, fieldId, value }]) => + customers.saveCustomField(customerId, fieldId, value), + removeCustomField: (...[, [{ customerId, fieldId }]]) => + customers.removeCustomField(customerId, fieldId), editCustomer: async (root, { customerId, customerEdit }, context) => { const token = authentication.getToken(context) const editedData = await customerEdit return customers.edit(customerId, editedData, token) }, - replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => { + replacePhoto: async ( + root, + { customerId, photoType, newPhoto }, + context, + ) => { const token = authentication.getToken(context) const { file } = newPhoto const photo = await file if (!photo) return customers.getCustomerById(customerId) - return customers.updateEditedPhoto(customerId, photo, photoType) + return customers + .updateEditedPhoto(customerId, photo, photoType) .then(newPatch => customers.edit(customerId, newPatch, token)) }, - deleteEditedData: (root, { customerId, customerEdit }) => { + deleteEditedData: (root, { customerId }) => { // TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION return customers.getCustomerById(customerId) }, @@ -55,12 +70,13 @@ const resolvers = { deleteCustomerNote: (...[, { noteId }]) => { return customerNotes.deleteCustomerNote(noteId) }, - createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }), + createCustomer: (...[, { phoneNumber }]) => + customers.add({ phone: phoneNumber }), enableTestCustomer: (...[, { customerId }]) => customers.enableTestCustomer(customerId), disableTestCustomer: (...[, { customerId }]) => - customers.disableTestCustomer(customerId) - } + customers.disableTestCustomer(customerId), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/funding.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/funding.resolver.js index b9b1c16c..abe217ec 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/funding.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/funding.resolver.js @@ -2,8 +2,8 @@ const funding = require('../../services/funding') const resolvers = { Query: { - funding: () => funding.getFunding() - } + funding: () => funding.getFunding(), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/index.js b/packages/server/lib/new-admin/graphql/resolvers/index.js index ea3cb3fa..d7656198 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/index.js +++ b/packages/server/lib/new-admin/graphql/resolvers/index.js @@ -47,7 +47,7 @@ const resolvers = [ status, transaction, user, - version + version, ] module.exports = mergeResolvers(resolvers) diff --git a/packages/server/lib/new-admin/graphql/resolvers/log.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/log.resolver.js index 3ed9276a..543b83ab 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/log.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/log.resolver.js @@ -1,5 +1,4 @@ const { parseAsync } = require('json2csv') -const _ = require('lodash/fp') const logs = require('../../../logs') const serverLogs = require('../../services/server-logs') @@ -8,15 +7,23 @@ const resolvers = { Query: { machineLogs: (...[, { deviceId, from, until, limit, offset }]) => logs.simpleGetMachineLogs(deviceId, from, until, limit, offset), - machineLogsCsv: (...[, { deviceId, from, until, limit, offset, timezone }]) => - logs.simpleGetMachineLogs(deviceId, from, until, limit, offset) - .then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp']))), + machineLogsCsv: ( + ...[, { deviceId, from, until, limit, offset, timezone }] + ) => + logs + .simpleGetMachineLogs(deviceId, from, until, limit, offset) + .then(res => + parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])), + ), serverLogs: (...[, { from, until, limit, offset }]) => serverLogs.getServerLogs(from, until, limit, offset), serverLogsCsv: (...[, { from, until, limit, offset, timezone }]) => - serverLogs.getServerLogs(from, until, limit, offset) - .then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp']))) - } + serverLogs + .getServerLogs(from, until, limit, offset) + .then(res => + parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])), + ), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/loyalty.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/loyalty.resolver.js index dd63da4b..532d458a 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/loyalty.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/loyalty.resolver.js @@ -3,24 +3,30 @@ const DataLoader = require('dataloader') const loyalty = require('../../../loyalty') const { getSlimCustomerByIdBatch } = require('../../../customers') -const customerLoader = new DataLoader(ids => { - return getSlimCustomerByIdBatch(ids) -}, { cache: false }) +const customerLoader = new DataLoader( + ids => { + return getSlimCustomerByIdBatch(ids) + }, + { cache: false }, +) const resolvers = { IndividualDiscount: { - customer: parent => customerLoader.load(parent.customerId) + customer: parent => customerLoader.load(parent.customerId), }, Query: { promoCodes: () => loyalty.getAvailablePromoCodes(), - individualDiscounts: () => loyalty.getAvailableIndividualDiscounts() + individualDiscounts: () => loyalty.getAvailableIndividualDiscounts(), }, Mutation: { - createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount), + createPromoCode: (...[, { code, discount }]) => + loyalty.createPromoCode(code, discount), deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId), - createIndividualDiscount: (...[, { customerId, discount }]) => loyalty.createIndividualDiscount(customerId, discount), - deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId) - } + createIndividualDiscount: (...[, { customerId, discount }]) => + loyalty.createIndividualDiscount(customerId, discount), + deleteIndividualDiscount: (...[, { discountId }]) => + loyalty.deleteIndividualDiscount(discountId), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/machine.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/machine.resolver.js index ebfbeaa2..1a404014 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/machine.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/machine.resolver.js @@ -3,25 +3,29 @@ const DataLoader = require('dataloader') const { machineAction } = require('../../services/machines') const machineLoader = require('../../../machine-loader') -const machineEventsByIdBatch = require('../../../postgresql_interface').machineEventsByIdBatch +const machineEventsByIdBatch = + require('../../../postgresql_interface').machineEventsByIdBatch -const machineEventsLoader = new DataLoader(ids => { - return machineEventsByIdBatch(ids) -}, { cache: false }) +const machineEventsLoader = new DataLoader( + ids => { + return machineEventsByIdBatch(ids) + }, + { cache: false }, +) const resolvers = { Machine: { - latestEvent: parent => machineEventsLoader.load(parent.deviceId) + latestEvent: parent => machineEventsLoader.load(parent.deviceId), }, Query: { machines: () => machineLoader.getMachineNames(), machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId), - unpairedMachines: () => machineLoader.getUnpairedMachines() + unpairedMachines: () => machineLoader.getUnpairedMachines(), }, Mutation: { machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) => - machineAction({ deviceId, action, cashUnits, newName }, context) - } + machineAction({ deviceId, action, cashUnits, newName }, context), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/market.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/market.resolver.js index 49864417..56f178b7 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/market.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/market.resolver.js @@ -2,8 +2,8 @@ const exchange = require('../../../exchange') const resolvers = { Query: { - getMarkets: () => exchange.getMarkets() - } + getMarkets: () => exchange.getMarkets(), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/notification.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/notification.resolver.js index 4a2db72a..d7dcd705 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/notification.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/notification.resolver.js @@ -4,12 +4,13 @@ const resolvers = { Query: { notifications: () => notifierQueries.getNotifications(), hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications(), - alerts: () => notifierQueries.getAlerts() + alerts: () => notifierQueries.getAlerts(), }, Mutation: { - toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read), - clearAllNotifications: () => notifierQueries.markAllAsRead() - } + toggleClearNotification: (...[, { id, read }]) => + notifierQueries.setRead(id, read), + clearAllNotifications: () => notifierQueries.markAllAsRead(), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/pairing.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/pairing.resolver.js index 510a6ef1..c3d35565 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/pairing.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/pairing.resolver.js @@ -2,8 +2,8 @@ const pairing = require('../../services/pairing') const resolvers = { Mutation: { - createPairingTotem: (...[, { name }]) => pairing.totem(name) - } + createPairingTotem: (...[, { name }]) => pairing.totem(name), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/rates.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/rates.resolver.js index dffa74e1..80751a7a 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/rates.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/rates.resolver.js @@ -10,12 +10,12 @@ const resolvers = { return pi.getRawRates().then(r => { return { withCommissions: pi.buildRates(r), - withoutCommissions: pi.buildRatesNoCommission(r) + withoutCommissions: pi.buildRatesNoCommission(r), } }) }), - fiatRates: () => forex.getFiatRates() - } + fiatRates: () => forex.getFiatRates(), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/sanctions.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/sanctions.resolver.js index bb1a8862..236cc63f 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/sanctions.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/sanctions.resolver.js @@ -6,8 +6,8 @@ const resolvers = { checkAgainstSanctions: (...[, { customerId }, context]) => { const token = authentication.getToken(context) return sanctions.checkByUser(customerId, token) - } - } + }, + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/scalar.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/scalar.resolver.js index ddfc12ac..2c6cbdba 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/scalar.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/scalar.resolver.js @@ -1,9 +1,13 @@ -const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars') +const { + DateTimeISOResolver, + JSONResolver, + JSONObjectResolver, +} = require('graphql-scalars') const resolvers = { JSON: JSONResolver, JSONObject: JSONObjectResolver, - DateTimeISO: DateTimeISOResolver + DateTimeISO: DateTimeISOResolver, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/settings.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/settings.resolver.js index e0453511..551cfaee 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/settings.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/settings.resolver.js @@ -3,12 +3,13 @@ const settingsLoader = require('../../../new-settings-loader') const resolvers = { Query: { accounts: () => settingsLoader.showAccounts(), - config: () => settingsLoader.loadLatestConfigOrNone() + config: () => settingsLoader.loadLatestConfigOrNone(), }, Mutation: { - saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts), + saveAccounts: (...[, { accounts }]) => + settingsLoader.saveAccounts(accounts), saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config), - } + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/sms.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/sms.resolver.js index 8098837b..df445e4a 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/sms.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/sms.resolver.js @@ -2,13 +2,14 @@ const smsNotices = require('../../../sms-notices') const resolvers = { Query: { - SMSNotices: () => smsNotices.getSMSNotices() + SMSNotices: () => smsNotices.getSMSNotices(), }, Mutation: { - editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message), + editSMSNotice: (...[, { id, event, message }]) => + smsNotices.editSMSNotice(id, event, message), enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id), - disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id) - } + disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/status.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/status.resolver.js index f357b78b..5a4930a4 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/status.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/status.resolver.js @@ -2,8 +2,8 @@ const supervisor = require('../../services/supervisor') const resolvers = { Query: { - uptime: () => supervisor.getAllProcessInfo() - } + uptime: () => supervisor.getAllProcessInfo(), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/transaction.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/transaction.resolver.js index 12e7e2b6..d5c3b9df 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/transaction.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/transaction.resolver.js @@ -1,6 +1,5 @@ const DataLoader = require('dataloader') const { parseAsync } = require('json2csv') -const _ = require('lodash/fp') const filters = require('../../filters') const cashOutTx = require('../../../cash-out/cash-out-tx') @@ -9,35 +8,124 @@ const transactions = require('../../services/transactions') const anonymous = require('../../../constants').anonymousCustomer const logDateFormat = require('../../../logs').logDateFormat -const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids), { cache: false }) +const transactionsLoader = new DataLoader( + ids => transactions.getCustomerTransactionsBatch(ids), + { cache: false }, +) const resolvers = { Customer: { - transactions: parent => transactionsLoader.load(parent.id) + transactions: parent => transactionsLoader.load(parent.id), }, Transaction: { - isAnonymous: parent => (parent.customerId === anonymous.uuid) + isAnonymous: parent => parent.customerId === anonymous.uuid, }, Query: { - transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) => - transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers), - transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) => - transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified) - .then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))), + transactions: ( + ...[ + , + { + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + excludeTestingCustomers, + }, + ] + ) => + transactions.batch( + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + excludeTestingCustomers, + ), + transactionsCsv: ( + ...[ + , + { + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + timezone, + excludeTestingCustomers, + simplified, + }, + ] + ) => + transactions + .batch( + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + excludeTestingCustomers, + simplified, + ) + .then(data => + parseAsync( + logDateFormat(timezone, data, [ + 'created', + 'sendTime', + 'publishedAt', + ]), + ), + ), transactionCsv: (...[, { id, txClass, timezone }]) => - transactions.getTx(id, txClass).then(data => - parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt'])) - ), + transactions + .getTx(id, txClass) + .then(data => + parseAsync( + logDateFormat( + timezone, + [data], + ['created', 'sendTime', 'publishedAt'], + ), + ), + ), txAssociatedDataCsv: (...[, { id, txClass, timezone }]) => - transactions.getTxAssociatedData(id, txClass).then(data => - parseAsync(logDateFormat(timezone, data, ['created'])) - ), - transactionFilters: () => filters.transaction() + transactions + .getTxAssociatedData(id, txClass) + .then(data => parseAsync(logDateFormat(timezone, data, ['created']))), + transactionFilters: () => filters.transaction(), }, Mutation: { cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id), - cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id) - } + cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id), + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js index c62bc8e0..513a341c 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js @@ -19,7 +19,11 @@ const getAttestationQueryOptions = variables => { const getAssertionQueryOptions = variables => { switch (authentication.CHOSEN_STRATEGY) { case 'FIDO2FA': - return { username: variables.username, password: variables.password, domain: variables.domain } + return { + username: variables.username, + password: variables.password, + domain: variables.domain, + } case 'FIDOPasswordless': return { username: variables.username, domain: variables.domain } case 'FIDOUsernameless': @@ -32,11 +36,23 @@ const getAssertionQueryOptions = variables => { const getAttestationMutationOptions = variables => { switch (authentication.CHOSEN_STRATEGY) { case 'FIDO2FA': - return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain } + return { + userId: variables.userID, + attestationResponse: variables.attestationResponse, + domain: variables.domain, + } case 'FIDOPasswordless': - return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain } + return { + userId: variables.userID, + attestationResponse: variables.attestationResponse, + domain: variables.domain, + } case 'FIDOUsernameless': - return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain } + return { + userId: variables.userID, + attestationResponse: variables.attestationResponse, + domain: variables.domain, + } default: return {} } @@ -45,11 +61,25 @@ const getAttestationMutationOptions = variables => { const getAssertionMutationOptions = variables => { switch (authentication.CHOSEN_STRATEGY) { case 'FIDO2FA': - return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain } + return { + username: variables.username, + password: variables.password, + rememberMe: variables.rememberMe, + assertionResponse: variables.assertionResponse, + domain: variables.domain, + } case 'FIDOPasswordless': - return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain } + return { + username: variables.username, + rememberMe: variables.rememberMe, + assertionResponse: variables.assertionResponse, + domain: variables.domain, + } case 'FIDOUsernameless': - return { assertionResponse: variables.assertionResponse, domain: variables.domain } + return { + assertionResponse: variables.assertionResponse, + domain: variables.domain, + } default: return {} } @@ -59,34 +89,82 @@ const resolver = { Query: { users: () => users.getUsers(), sessions: () => sessionManager.getSessions(), - userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username), - userData: (...[, {}, context]) => userManagement.getUserData(context), - get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password), - confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context), - validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token), - validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token), - validateReset2FALink: (...[, { token }]) => userManagement.validateReset2FALink(token), - generateAttestationOptions: (...[, variables, context]) => authentication.strategy.generateAttestationOptions(context.req.session, getAttestationQueryOptions(variables)), - generateAssertionOptions: (...[, variables, context]) => authentication.strategy.generateAssertionOptions(context.req.session, getAssertionQueryOptions(variables)) + userSessions: (...[, { username }]) => + sessionManager.getSessionsByUsername(username), + userData: (...[, , context]) => userManagement.getUserData(context), + get2FASecret: (...[, { username, password }]) => + userManagement.get2FASecret(username, password), + confirm2FA: (...[, { code }, context]) => + userManagement.confirm2FA(code, context), + validateRegisterLink: (...[, { token }]) => + userManagement.validateRegisterLink(token), + validateResetPasswordLink: (...[, { token }]) => + userManagement.validateResetPasswordLink(token), + validateReset2FALink: (...[, { token }]) => + userManagement.validateReset2FALink(token), + generateAttestationOptions: (...[, variables, context]) => + authentication.strategy.generateAttestationOptions( + context.req.session, + getAttestationQueryOptions(variables), + ), + generateAssertionOptions: (...[, variables, context]) => + authentication.strategy.generateAssertionOptions( + context.req.session, + getAssertionQueryOptions(variables), + ), }, Mutation: { - enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context), - disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context), - deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context), - deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username), - changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context), - login: (...[, { username, password }]) => userManagement.login(username, password), - input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context), - setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context), - createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context), - createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context), - createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role), - register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role), - resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context), - reset2FA: (...[, { token, userID, code }, context]) => userManagement.reset2FA(token, userID, code, context), - validateAttestation: (...[, variables, context]) => authentication.strategy.validateAttestation(context.req.session, getAttestationMutationOptions(variables)), - validateAssertion: (...[, variables, context]) => authentication.strategy.validateAssertion(context.req.session, getAssertionMutationOptions(variables)) - } + enableUser: (...[, { confirmationCode, id }, context]) => + userManagement.enableUser(confirmationCode, id, context), + disableUser: (...[, { confirmationCode, id }, context]) => + userManagement.disableUser(confirmationCode, id, context), + deleteSession: (...[, { sid }, context]) => + userManagement.deleteSession(sid, context), + deleteUserSessions: (...[, { username }]) => + sessionManager.deleteSessionsByUsername(username), + changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => + userManagement.changeUserRole(confirmationCode, id, newRole, context), + login: (...[, { username, password }]) => + userManagement.login(username, password), + input2FA: (...[, { username, password, rememberMe, code }, context]) => + userManagement.input2FA(username, password, rememberMe, code, context), + setup2FA: ( + ...[, { username, password, rememberMe, codeConfirmation }, context] + ) => + userManagement.setup2FA( + username, + password, + rememberMe, + codeConfirmation, + context, + ), + createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => + userManagement.createResetPasswordToken( + confirmationCode, + userID, + context, + ), + createReset2FAToken: (...[, { confirmationCode, userID }, context]) => + userManagement.createReset2FAToken(confirmationCode, userID, context), + createRegisterToken: (...[, { username, role }]) => + userManagement.createRegisterToken(username, role), + register: (...[, { token, username, password, role }]) => + userManagement.register(token, username, password, role), + resetPassword: (...[, { token, userID, newPassword }, context]) => + userManagement.resetPassword(token, userID, newPassword, context), + reset2FA: (...[, { token, userID, code }, context]) => + userManagement.reset2FA(token, userID, code, context), + validateAttestation: (...[, variables, context]) => + authentication.strategy.validateAttestation( + context.req.session, + getAttestationMutationOptions(variables), + ), + validateAssertion: (...[, variables, context]) => + authentication.strategy.validateAssertion( + context.req.session, + getAssertionMutationOptions(variables), + ), + }, } module.exports = resolver diff --git a/packages/server/lib/new-admin/graphql/resolvers/version.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/version.resolver.js index d6f5b1f1..aa3e564c 100644 --- a/packages/server/lib/new-admin/graphql/resolvers/version.resolver.js +++ b/packages/server/lib/new-admin/graphql/resolvers/version.resolver.js @@ -2,8 +2,8 @@ const serverVersion = require('../../../../package.json').version const resolvers = { Query: { - serverVersion: () => serverVersion - } + serverVersion: () => serverVersion, + }, } module.exports = resolvers diff --git a/packages/server/lib/new-admin/graphql/schema.js b/packages/server/lib/new-admin/graphql/schema.js index 68bd4257..218c3e91 100644 --- a/packages/server/lib/new-admin/graphql/schema.js +++ b/packages/server/lib/new-admin/graphql/schema.js @@ -3,5 +3,5 @@ const resolvers = require('./resolvers') module.exports = { resolvers: resolvers, - typeDefs: types + typeDefs: types, } diff --git a/packages/server/lib/new-admin/graphql/types/cashbox.type.js b/packages/server/lib/new-admin/graphql/types/cashbox.type.js index ada0441d..35b6d933 100644 --- a/packages/server/lib/new-admin/graphql/types/cashbox.type.js +++ b/packages/server/lib/new-admin/graphql/types/cashbox.type.js @@ -14,7 +14,11 @@ const typeDef = gql` type Query { cashboxBatches: [CashboxBatch] @auth - cashboxBatchesCsv(from: DateTimeISO, until: DateTimeISO, timezone: String): String @auth + cashboxBatchesCsv( + from: DateTimeISO + until: DateTimeISO + timezone: String + ): String @auth } type Mutation { diff --git a/packages/server/lib/new-admin/graphql/types/customInfoRequests.type.js b/packages/server/lib/new-admin/graphql/types/customInfoRequests.type.js index 88d2def3..cbd33d6a 100644 --- a/packages/server/lib/new-admin/graphql/types/customInfoRequests.type.js +++ b/packages/server/lib/new-admin/graphql/types/customInfoRequests.type.js @@ -1,10 +1,9 @@ const gql = require('graphql-tag') const typeDef = gql` - type CustomInfoRequest { - id: ID!, - enabled: Boolean, + id: ID! + enabled: Boolean customRequest: JSON } @@ -42,15 +41,31 @@ const typeDef = gql` type Query { customInfoRequests(onlyEnabled: Boolean): [CustomInfoRequest] @auth customerCustomInfoRequests(customerId: ID!): [CustomRequestData] @auth - customerCustomInfoRequest(customerId: ID!, infoRequestId: ID!): CustomRequestData @auth + customerCustomInfoRequest( + customerId: ID! + infoRequestId: ID! + ): CustomRequestData @auth } type Mutation { - insertCustomInfoRequest(customRequest: CustomRequestInput!): CustomInfoRequest @auth + insertCustomInfoRequest( + customRequest: CustomRequestInput! + ): CustomInfoRequest @auth removeCustomInfoRequest(id: ID!): CustomInfoRequest @auth - editCustomInfoRequest(id: ID!, customRequest: CustomRequestInput!): CustomInfoRequest @auth - setAuthorizedCustomRequest(customerId: ID!, infoRequestId: ID!, override: String!): Boolean @auth - setCustomerCustomInfoRequest(customerId: ID!, infoRequestId: ID!, data: JSON!): Boolean @auth + editCustomInfoRequest( + id: ID! + customRequest: CustomRequestInput! + ): CustomInfoRequest @auth + setAuthorizedCustomRequest( + customerId: ID! + infoRequestId: ID! + override: String! + ): Boolean @auth + setCustomerCustomInfoRequest( + customerId: ID! + infoRequestId: ID! + data: JSON! + ): Boolean @auth } ` diff --git a/packages/server/lib/new-admin/graphql/types/customer.type.js b/packages/server/lib/new-admin/graphql/types/customer.type.js index 3c6b00fd..f6d16bf2 100644 --- a/packages/server/lib/new-admin/graphql/types/customer.type.js +++ b/packages/server/lib/new-admin/graphql/types/customer.type.js @@ -95,20 +95,37 @@ const typeDef = gql` } type Query { - customers(phone: String, name: String, email: String, address: String, id: String): [Customer] @auth + customers( + phone: String + name: String + email: String + address: String + id: String + ): [Customer] @auth customer(customerId: ID!): Customer @auth customerFilters: [Filter] @auth } type Mutation { setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth - addCustomField(customerId: ID!, label: String!, value: String!): Boolean @auth - saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean @auth + addCustomField(customerId: ID!, label: String!, value: String!): Boolean + @auth + saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean + @auth removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth - deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth - replacePhoto(customerId: ID!, photoType: String, newPhoto: Upload): Customer @auth - createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth + deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer + @auth + replacePhoto( + customerId: ID! + photoType: String + newPhoto: Upload + ): Customer @auth + createCustomerNote( + customerId: ID! + title: String! + content: String! + ): Boolean @auth editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth deleteCustomerNote(noteId: ID!): Boolean @auth createCustomer(phoneNumber: String): Customer @auth diff --git a/packages/server/lib/new-admin/graphql/types/index.js b/packages/server/lib/new-admin/graphql/types/index.js index e33c50b5..6460a8bb 100644 --- a/packages/server/lib/new-admin/graphql/types/index.js +++ b/packages/server/lib/new-admin/graphql/types/index.js @@ -47,7 +47,7 @@ const types = [ status, transaction, user, - version + version, ] module.exports = mergeTypeDefs(types) diff --git a/packages/server/lib/new-admin/graphql/types/log.type.js b/packages/server/lib/new-admin/graphql/types/log.type.js index de7aee38..5d10fdfb 100644 --- a/packages/server/lib/new-admin/graphql/types/log.type.js +++ b/packages/server/lib/new-admin/graphql/types/log.type.js @@ -16,10 +16,34 @@ const typeDef = gql` } type Query { - machineLogs(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [MachineLog] @auth - machineLogsCsv(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth - serverLogs(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [ServerLog] @auth - serverLogsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth + machineLogs( + deviceId: ID! + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + ): [MachineLog] @auth + machineLogsCsv( + deviceId: ID! + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + timezone: String + ): String @auth + serverLogs( + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + ): [ServerLog] @auth + serverLogsCsv( + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + timezone: String + ): String @auth } ` diff --git a/packages/server/lib/new-admin/graphql/types/loyalty.type.js b/packages/server/lib/new-admin/graphql/types/loyalty.type.js index 0227a503..862f5545 100644 --- a/packages/server/lib/new-admin/graphql/types/loyalty.type.js +++ b/packages/server/lib/new-admin/graphql/types/loyalty.type.js @@ -6,7 +6,7 @@ const typeDef = gql` customer: DiscountCustomer! discount: Int } - + type DiscountCustomer { id: ID! phone: String @@ -27,7 +27,10 @@ const typeDef = gql` type Mutation { createPromoCode(code: String!, discount: Int!): PromoCode @auth deletePromoCode(codeId: ID!): PromoCode @auth - createIndividualDiscount(customerId: ID!, discount: Int!): IndividualDiscount @auth + createIndividualDiscount( + customerId: ID! + discount: Int! + ): IndividualDiscount @auth deleteIndividualDiscount(discountId: ID!): IndividualDiscount @auth } ` diff --git a/packages/server/lib/new-admin/graphql/types/machine.type.js b/packages/server/lib/new-admin/graphql/types/machine.type.js index d157913c..387e2f2d 100644 --- a/packages/server/lib/new-admin/graphql/types/machine.type.js +++ b/packages/server/lib/new-admin/graphql/types/machine.type.js @@ -30,7 +30,7 @@ const typeDef = gql` frontTimestamp: DateTimeISO scanTimestamp: DateTimeISO } - + type CashUnits { cashbox: Int cassette1: Int @@ -98,7 +98,12 @@ const typeDef = gql` } type Mutation { - machineAction(deviceId:ID!, action: MachineAction!, cashUnits: CashUnitsInput, newName: String): Machine @auth + machineAction( + deviceId: ID! + action: MachineAction! + cashUnits: CashUnitsInput + newName: String + ): Machine @auth } ` diff --git a/packages/server/lib/new-admin/graphql/types/sms.type.js b/packages/server/lib/new-admin/graphql/types/sms.type.js index e134a20d..36375204 100644 --- a/packages/server/lib/new-admin/graphql/types/sms.type.js +++ b/packages/server/lib/new-admin/graphql/types/sms.type.js @@ -21,7 +21,8 @@ const typeDef = gql` } type Mutation { - editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice @auth + editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice + @auth enableSMSNotice(id: ID!): SMSNotice @auth disableSMSNotice(id: ID!): SMSNotice @auth } diff --git a/packages/server/lib/new-admin/graphql/types/transaction.type.js b/packages/server/lib/new-admin/graphql/types/transaction.type.js index 9edde74b..40193c75 100644 --- a/packages/server/lib/new-admin/graphql/types/transaction.type.js +++ b/packages/server/lib/new-admin/graphql/types/transaction.type.js @@ -60,8 +60,38 @@ const typeDef = gql` } type Query { - transactions(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth - transactionsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth + transactions( + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + txClass: String + deviceId: String + customerName: String + fiatCode: String + cryptoCode: String + toAddress: String + status: String + swept: Boolean + excludeTestingCustomers: Boolean + ): [Transaction] @auth + transactionsCsv( + from: DateTimeISO + until: DateTimeISO + limit: Int + offset: Int + txClass: String + deviceId: String + customerName: String + fiatCode: String + cryptoCode: String + toAddress: String + status: String + swept: Boolean + timezone: String + excludeTestingCustomers: Boolean + simplified: Boolean + ): String @auth transactionCsv(id: ID, txClass: String, timezone: String): String @auth txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth transactionFilters: [Filter] @auth diff --git a/packages/server/lib/new-admin/middlewares/cleanUserSessions.js b/packages/server/lib/new-admin/middlewares/cleanUserSessions.js index 5372b412..8fa45d41 100644 --- a/packages/server/lib/new-admin/middlewares/cleanUserSessions.js +++ b/packages/server/lib/new-admin/middlewares/cleanUserSessions.js @@ -4,13 +4,17 @@ const logger = require('../../logger') let schemaCache = Date.now() -const cleanUserSessions = (cleanInterval) => (req, res, next) => { +const cleanUserSessions = cleanInterval => (req, res, next) => { const now = Date.now() if (schemaCache + cleanInterval > now) return next() logger.debug(`Clearing expired sessions for schema 'public'`) - return db.none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [USER_SESSIONS_TABLE_NAME, now]) + return db + .none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [ + USER_SESSIONS_TABLE_NAME, + now, + ]) .then(() => { schemaCache = now return next() diff --git a/packages/server/lib/new-admin/middlewares/context.js b/packages/server/lib/new-admin/middlewares/context.js index 6db47245..60fa5d5f 100644 --- a/packages/server/lib/new-admin/middlewares/context.js +++ b/packages/server/lib/new-admin/middlewares/context.js @@ -8,9 +8,10 @@ const buildApolloContext = async ({ req, res }) => { const user = await users.verifyAndUpdateUser( req.session.user.id, req.headers['user-agent'] || 'Unknown', - req.ip + req.ip, ) - if (!user || !user.enabled) throw new AuthenticationError('Authentication failed') + if (!user || !user.enabled) + throw new AuthenticationError('Authentication failed') req.session.ua = req.headers['user-agent'] || 'Unknown' req.session.ipAddress = req.ip diff --git a/packages/server/lib/new-admin/middlewares/index.js b/packages/server/lib/new-admin/middlewares/index.js index cedba31c..3eea6f7b 100644 --- a/packages/server/lib/new-admin/middlewares/index.js +++ b/packages/server/lib/new-admin/middlewares/index.js @@ -5,5 +5,5 @@ const session = require('./session') module.exports = { cleanUserSessions, buildApolloContext, - session + session, } diff --git a/packages/server/lib/new-admin/middlewares/session.js b/packages/server/lib/new-admin/middlewares/session.js index 2c0400af..fb8991e7 100644 --- a/packages/server/lib/new-admin/middlewares/session.js +++ b/packages/server/lib/new-admin/middlewares/session.js @@ -6,21 +6,24 @@ const db = require('../../db') const { USER_SESSIONS_TABLE_NAME } = require('../../constants') const { getOperatorId } = require('../../operator') -router.use('*', async (req, res, next) => getOperatorId('authentication').then(operatorId => session({ - store: new PgSession({ - pgPromise: db, - tableName: USER_SESSIONS_TABLE_NAME - }), - name: 'lamassu_sid', - secret: operatorId, - resave: false, - saveUninitialized: false, - cookie: { - httpOnly: true, - secure: true, - sameSite: true - } -})(req, res, next)) +router.use('*', async (req, res, next) => + getOperatorId('authentication').then(operatorId => + session({ + store: new PgSession({ + pgPromise: db, + tableName: USER_SESSIONS_TABLE_NAME, + }), + name: 'lamassu_sid', + secret: operatorId, + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: true, + sameSite: true, + }, + })(req, res, next), + ), ) module.exports = router diff --git a/packages/server/lib/new-admin/services/bills.js b/packages/server/lib/new-admin/services/bills.js index d79d7002..78092ab8 100644 --- a/packages/server/lib/new-admin/services/bills.js +++ b/packages/server/lib/new-admin/services/bills.js @@ -4,7 +4,9 @@ const pgp = require('pg-promise')() const db = require('../../db') const getBills = filters => { - const deviceStatement = !_.isNil(filters.deviceId) ? `WHERE device_id = ${pgp.as.text(filters.deviceId)}` : `` + const deviceStatement = !_.isNil(filters.deviceId) + ? `WHERE device_id = ${pgp.as.text(filters.deviceId)}` + : `` const batchStatement = filter => { switch (filter) { case 'none': @@ -12,7 +14,9 @@ const getBills = filters => { case 'any': return `WHERE b.cashbox_batch_id IS NOT NULL` default: - return _.isNil(filter) ? `` : `WHERE b.cashbox_batch_id = ${pgp.as.text(filter)}` + return _.isNil(filter) + ? `` + : `WHERE b.cashbox_batch_id = ${pgp.as.text(filter)}` } } @@ -22,10 +26,12 @@ const getBills = filters => { const sql2 = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, b.device_id FROM empty_unit_bills b ${deviceStatement} ${!_.isNil(filters.deviceId) && !_.isNil(filters.batch) ? `AND ${_.replace('WHERE', '', batchStatement(filters.batch))}` : `${batchStatement(filters.batch)}`}` - return Promise.all([db.any(sql), db.any(sql2)]) - .then(([bills, operationalBills]) => _.map(_.mapKeys(_.camelCase), _.concat(bills, operationalBills))) + return Promise.all([db.any(sql), db.any(sql2)]).then( + ([bills, operationalBills]) => + _.map(_.mapKeys(_.camelCase), _.concat(bills, operationalBills)), + ) } module.exports = { - getBills + getBills, } diff --git a/packages/server/lib/new-admin/services/customInfoRequests.js b/packages/server/lib/new-admin/services/customInfoRequests.js index fff8dc39..daec3480 100644 --- a/packages/server/lib/new-admin/services/customInfoRequests.js +++ b/packages/server/lib/new-admin/services/customInfoRequests.js @@ -2,7 +2,10 @@ const db = require('../../db') const uuid = require('uuid') const _ = require('lodash/fp') const pgp = require('pg-promise')() -const { loadLatestConfigOrNone, saveConfig } = require('../../../lib/new-settings-loader') +const { + loadLatestConfigOrNone, + saveConfig, +} = require('../../../lib/new-settings-loader') const getCustomInfoRequests = (onlyEnabled = false) => { const sql = onlyEnabled @@ -12,38 +15,57 @@ const getCustomInfoRequests = (onlyEnabled = false) => { return res.map(item => ({ id: item.id, enabled: item.enabled, - customRequest: item.custom_request + customRequest: item.custom_request, })) }) } -const addCustomInfoRequest = (customRequest) => { - const sql = 'INSERT INTO custom_info_requests (id, custom_request) VALUES ($1, $2)' +const addCustomInfoRequest = customRequest => { + const sql = + 'INSERT INTO custom_info_requests (id, custom_request) VALUES ($1, $2)' const id = uuid.v4() return db.none(sql, [id, customRequest]).then(() => ({ id })) } -const removeCustomInfoRequest = (id) => { +const removeCustomInfoRequest = id => { return loadLatestConfigOrNone() - .then(cfg => saveConfig({triggers: _.remove(x => x.customInfoRequestId === id, cfg.triggers ?? [])})) - .then(() => db.none('UPDATE custom_info_requests SET enabled = false WHERE id = $1', [id])) - .then(() => ({ id })); + .then(cfg => + saveConfig({ + triggers: _.remove( + x => x.customInfoRequestId === id, + cfg.triggers ?? [], + ), + }), + ) + .then(() => + db.none('UPDATE custom_info_requests SET enabled = false WHERE id = $1', [ + id, + ]), + ) + .then(() => ({ id })) } const editCustomInfoRequest = (id, customRequest) => { - return db.none('UPDATE custom_info_requests SET custom_request = $1 WHERE id=$2', [customRequest, id]).then(() => ({ id, customRequest })) + return db + .none('UPDATE custom_info_requests SET custom_request = $1 WHERE id=$2', [ + customRequest, + id, + ]) + .then(() => ({ id, customRequest })) } -const getAllCustomInfoRequestsForCustomer = (customerId) => { +const getAllCustomInfoRequestsForCustomer = customerId => { const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1` - return db.any(sql, [customerId]).then(res => res.map(item => ({ - customerId: item.customer_id, - infoRequestId: item.info_request_id, - customerData: item.customer_data, - override: item.override, - overrideAt: item.override_at, - overrideBy: item.override_by - }))) + return db.any(sql, [customerId]).then(res => + res.map(item => ({ + customerId: item.customer_id, + infoRequestId: item.info_request_id, + customerData: item.customer_data, + override: item.override, + overrideAt: item.override_at, + overrideBy: item.override_by, + })), + ) } const getCustomInfoRequestForCustomer = (customerId, infoRequestId) => { @@ -55,12 +77,12 @@ const getCustomInfoRequestForCustomer = (customerId, infoRequestId) => { customerData: item.customer_data, override: item.override, overrideAt: item.override_at, - overrideBy: item.override_by + overrideBy: item.override_by, } }) } -const batchGetAllCustomInfoRequestsForCustomer = (customerIds) => { +const batchGetAllCustomInfoRequestsForCustomer = customerIds => { const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id IN ($1^)` return db.any(sql, [_.map(pgp.as.text, customerIds).join(',')]).then(res => { const map = _.groupBy('customer_id', res) @@ -72,40 +94,49 @@ const batchGetAllCustomInfoRequestsForCustomer = (customerIds) => { customerData: item.customer_data, override: item.override, overrideAt: item.override_at, - overrideBy: item.override_by + overrideBy: item.override_by, })) }) }) } -const getCustomInfoRequest = (infoRequestId) => { +const getCustomInfoRequest = infoRequestId => { const sql = `SELECT * FROM custom_info_requests WHERE id = $1` return db.one(sql, [infoRequestId]).then(item => ({ id: item.id, enabled: item.enabled, - customRequest: item.custom_request + customRequest: item.custom_request, })) } -const batchGetCustomInfoRequest = (infoRequestIds) => { +const batchGetCustomInfoRequest = infoRequestIds => { if (infoRequestIds.length === 0) return Promise.resolve([]) const sql = `SELECT * FROM custom_info_requests WHERE id IN ($1^)` - return db.any(sql, [_.map(pgp.as.text, infoRequestIds).join(',')]).then(res => { - const map = _.groupBy('id', res) - return infoRequestIds.map(id => { - const item = map[id][0] // since id is primary key the array always has 1 element - return { - id: item.id, - enabled: item.enabled, - customRequest: item.custom_request - } + return db + .any(sql, [_.map(pgp.as.text, infoRequestIds).join(',')]) + .then(res => { + const map = _.groupBy('id', res) + return infoRequestIds.map(id => { + const item = map[id][0] // since id is primary key the array always has 1 element + return { + id: item.id, + enabled: item.enabled, + customRequest: item.custom_request, + } + }) }) - }) } -const setAuthorizedCustomRequest = (customerId, infoRequestId, override, token) => { +const setAuthorizedCustomRequest = ( + customerId, + infoRequestId, + override, + token, +) => { const sql = `UPDATE customers_custom_info_requests SET override = $1, override_by = $2, override_at = now() WHERE customer_id = $3 AND info_request_id = $4` - return db.none(sql, [override, token, customerId, infoRequestId]).then(() => true) + return db + .none(sql, [override, token, customerId, infoRequestId]) + .then(() => true) } const setCustomerData = (customerId, infoRequestId, data) => { @@ -138,5 +169,5 @@ module.exports = { batchGetCustomInfoRequest, setAuthorizedCustomRequest, setCustomerData, - setCustomerDataViaMachine + setCustomerDataViaMachine, } diff --git a/packages/server/lib/new-admin/services/funding.js b/packages/server/lib/new-admin/services/funding.js index 5b425d82..9d74d0d6 100644 --- a/packages/server/lib/new-admin/services/funding.js +++ b/packages/server/lib/new-admin/services/funding.js @@ -7,55 +7,65 @@ const ticker = require('../../ticker') const txBatching = require('../../tx-batching') const { utils: coinUtils } = require('@lamassu/coins') -function computeCrypto (cryptoCode, _balance) { +function computeCrypto(cryptoCode, _balance) { const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale return new BN(_balance).shiftedBy(-unitScale).decimalPlaces(5) } -function computeFiat (rate, cryptoCode, _balance) { +function computeFiat(rate, cryptoCode, _balance) { const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale return new BN(_balance).shiftedBy(-unitScale).times(rate).decimalPlaces(5) } -function getSingleCoinFunding (settings, fiatCode, cryptoCode) { +function getSingleCoinFunding(settings, fiatCode, cryptoCode) { const promises = [ wallet.newFunding(settings, cryptoCode), ticker.getRates(settings, fiatCode, cryptoCode), - txBatching.getOpenBatchCryptoValue(cryptoCode) + txBatching.getOpenBatchCryptoValue(cryptoCode), ] - return Promise.all(promises) - .then(([fundingRec, ratesRec, batchRec]) => { - const rates = ratesRec.rates - const rate = (rates.ask.plus(rates.bid)).div(2) - const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance - const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance) - const pending = fundingRec.fundingPendingBalance.minus(batchRec) - const fiatPending = computeFiat(rate, cryptoCode, pending) - const fundingAddress = fundingRec.fundingAddress - const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress) + return Promise.all(promises).then(([fundingRec, ratesRec, batchRec]) => { + const rates = ratesRec.rates + const rate = rates.ask.plus(rates.bid).div(2) + const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance + const fiatConfirmedBalance = computeFiat( + rate, + cryptoCode, + fundingConfirmedBalance, + ) + const pending = fundingRec.fundingPendingBalance.minus(batchRec) + const fiatPending = computeFiat(rate, cryptoCode, pending) + const fundingAddress = fundingRec.fundingAddress + const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress) - return { + return { + cryptoCode, + fundingAddress, + fundingAddressUrl, + confirmedBalance: computeCrypto( cryptoCode, - fundingAddress, - fundingAddressUrl, - confirmedBalance: computeCrypto(cryptoCode, fundingConfirmedBalance).toFormat(5), - pending: computeCrypto(cryptoCode, pending).toFormat(5), - fiatConfirmedBalance: fiatConfirmedBalance, - fiatPending: fiatPending, - fiatCode - } - }) + fundingConfirmedBalance, + ).toFormat(5), + pending: computeCrypto(cryptoCode, pending).toFormat(5), + fiatConfirmedBalance: fiatConfirmedBalance, + fiatPending: fiatPending, + fiatCode, + } + }) } // Promise.allSettled not running on current version of node -const reflect = p => p.then(value => ({ value, status: 'fulfilled' }), error => ({ error: error.toString(), status: 'rejected' })) +const reflect = p => + p.then( + value => ({ value, status: 'fulfilled' }), + error => ({ error: error.toString(), status: 'rejected' }), + ) -function getFunding () { +function getFunding() { return settingsLoader.loadLatest().then(settings => { const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config) const fiatCode = configManager.getGlobalLocale(settings.config).fiatCurrency @@ -63,12 +73,15 @@ function getFunding () { const cryptoCurrencies = coinUtils.cryptoCurrencies() const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies) - const promises = cryptoDisplays.map(it => getSingleCoinFunding(settings, fiatCode, it.cryptoCode)) - return Promise.all(promises.map(reflect)) - .then((response) => { - const mapped = response.map(it => _.merge({ errorMsg: it.error }, it.value)) - return _.toArray(_.merge(mapped, cryptoDisplays)) - }) + const promises = cryptoDisplays.map(it => + getSingleCoinFunding(settings, fiatCode, it.cryptoCode), + ) + return Promise.all(promises.map(reflect)).then(response => { + const mapped = response.map(it => + _.merge({ errorMsg: it.error }, it.value), + ) + return _.toArray(_.merge(mapped, cryptoDisplays)) + }) }) } diff --git a/packages/server/lib/new-admin/services/login.js b/packages/server/lib/new-admin/services/login.js index 8eb19c53..67fda521 100644 --- a/packages/server/lib/new-admin/services/login.js +++ b/packages/server/lib/new-admin/services/login.js @@ -1,16 +1,23 @@ const db = require('../../db') -function validateUser (username, password) { +function validateUser(username, password) { return db.tx(t => { - const q1 = t.one('SELECT * FROM users WHERE username=$1 AND password=$2', [username, password]) - const q2 = t.none('UPDATE users SET last_accessed = now() WHERE username=$1', [username]) + const q1 = t.one('SELECT * FROM users WHERE username=$1 AND password=$2', [ + username, + password, + ]) + const q2 = t.none( + 'UPDATE users SET last_accessed = now() WHERE username=$1', + [username], + ) - return t.batch([q1, q2]) + return t + .batch([q1, q2]) .then(([user]) => user) .catch(() => false) }) } module.exports = { - validateUser + validateUser, } diff --git a/packages/server/lib/new-admin/services/machines.js b/packages/server/lib/new-admin/services/machines.js index b280c9b9..386da1fd 100644 --- a/packages/server/lib/new-admin/services/machines.js +++ b/packages/server/lib/new-admin/services/machines.js @@ -1,19 +1,26 @@ const machineLoader = require('../../machine-loader') const { UserInputError } = require('../graphql/errors') -function getMachine (machineId) { - return machineLoader.getMachines() +function getMachine(machineId) { + return machineLoader + .getMachines() .then(machines => machines.find(({ deviceId }) => deviceId === machineId)) } -function machineAction ({ deviceId, action, cashUnits, newName }, context) { +function machineAction({ deviceId, action, cashUnits, newName }, context) { const operatorId = context.res.locals.operatorId return getMachine(deviceId) .then(machine => { - if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) + if (!machine) + throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) return machine }) - .then(machineLoader.setMachine({ deviceId, action, cashUnits, newName }, operatorId)) + .then( + machineLoader.setMachine( + { deviceId, action, cashUnits, newName }, + operatorId, + ), + ) .then(getMachine(deviceId)) } diff --git a/packages/server/lib/new-admin/services/pairing.js b/packages/server/lib/new-admin/services/pairing.js index d267c1a9..f3446447 100644 --- a/packages/server/lib/new-admin/services/pairing.js +++ b/packages/server/lib/new-admin/services/pairing.js @@ -3,7 +3,6 @@ const pify = require('pify') const readFile = pify(fs.readFile) const crypto = require('crypto') const baseX = require('base-x') -const { parse, NIL } = require('uuid') const db = require('../../db') const pairing = require('../../pairing') @@ -16,19 +15,23 @@ const HOSTNAME = process.env.HOSTNAME const unpair = pairing.unpair -function totem (name) { - 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(HOSTNAME)]) - const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)' +function totem(name) { + 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(HOSTNAME)]) + const sql = + 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)' - return db.none(sql, [hexToken, caHexToken, name]) - .then(() => bsAlpha.encode(buf)) - }) + return db + .none(sql, [hexToken, caHexToken, name]) + .then(() => bsAlpha.encode(buf)) + }) } module.exports = { totem, unpair } diff --git a/packages/server/lib/new-admin/services/server-logs.js b/packages/server/lib/new-admin/services/server-logs.js index 80d93c7f..1b4d4641 100644 --- a/packages/server/lib/new-admin/services/server-logs.js +++ b/packages/server/lib/new-admin/services/server-logs.js @@ -1,16 +1,21 @@ const _ = require('lodash/fp') -const uuid = require('uuid') const db = require('../../db') -function getServerLogs (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) { +function getServerLogs( + from = new Date(0).toISOString(), + until = new Date().toISOString(), + limit = null, + offset = 0, +) { const sql = `select id, log_level, timestamp, message from server_logs where timestamp >= $1 and timestamp <= $2 order by timestamp desc limit $3 offset $4` - return db.any(sql, [ from, until, limit, offset ]) + return db + .any(sql, [from, until, limit, offset]) .then(_.map(_.mapKeys(_.camelCase))) } diff --git a/packages/server/lib/new-admin/services/supervisor.js b/packages/server/lib/new-admin/services/supervisor.js index 30e6b3c0..a6d6f4c0 100644 --- a/packages/server/lib/new-admin/services/supervisor.js +++ b/packages/server/lib/new-admin/services/supervisor.js @@ -6,8 +6,8 @@ const { promisify } = require('util') // [inet_http_server] // port = 127.0.0.1:9001 -function getAllProcessInfo () { - const convertStates = (state) => { +function getAllProcessInfo() { + const convertStates = state => { // From http://supervisord.org/subprocess.html#process-states switch (state) { case 'STOPPED': @@ -33,28 +33,30 @@ function getAllProcessInfo () { const client = xmlrpc.createClient({ host: 'localhost', port: '9001', - path: '/RPC2' + path: '/RPC2', }) client.methodCall[promisify.custom] = (method, params) => { - return new Promise((resolve, reject) => client.methodCall(method, params, (err, value) => { - if (err) reject(err) - else resolve(value) - })) + return new Promise((resolve, reject) => + client.methodCall(method, params, (err, value) => { + if (err) reject(err) + else resolve(value) + }), + ) } return promisify(client.methodCall)('supervisor.getAllProcessInfo', []) - .then((value) => { - return value.map(process => ( - { - name: process.name, - state: convertStates(process.statename), - uptime: (process.statename === 'RUNNING') ? process.now - process.start : 0 - } - )) + .then(value => { + return value.map(process => ({ + name: process.name, + state: convertStates(process.statename), + uptime: + process.statename === 'RUNNING' ? process.now - process.start : 0, + })) }) - .catch((error) => { - if (error.code === 'ECONNREFUSED') logger.error('Failed to connect to supervisord HTTP server.') + .catch(error => { + if (error.code === 'ECONNREFUSED') + logger.error('Failed to connect to supervisord HTTP server.') else logger.error(error) }) } diff --git a/packages/server/lib/new-admin/services/transactions.js b/packages/server/lib/new-admin/services/transactions.js index 8806d954..4afbd6bc 100644 --- a/packages/server/lib/new-admin/services/transactions.js +++ b/packages/server/lib/new-admin/services/transactions.js @@ -4,14 +4,16 @@ const pgp = require('pg-promise')() const db = require('../../db') const BN = require('../../bn') const { utils: coinUtils } = require('@lamassu/coins') -const machineLoader = require('../../machine-loader') const tx = require('../../tx') const cashInTx = require('../../cash-in/cash-in-tx') -const { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/cash-out-helper') +const { + REDEEMABLE_AGE, + CASH_OUT_TRANSACTION_STATES, +} = require('../../cash-out/cash-out-helper') const NUM_RESULTS = 1000 -function addProfits (txs) { +function addProfits(txs) { return _.map(it => { const profit = getProfit(it).toString() return _.set('profit', profit, it) @@ -28,7 +30,7 @@ const DEVICE_NAME_QUERY = ` END AS machine_name ` - const DEVICE_NAME_JOINS = ` +const DEVICE_NAME_JOINS = ` LEFT JOIN devices d ON txs.device_id = d.device_id LEFT JOIN ( SELECT device_id, name, unpaired, paired @@ -38,7 +40,7 @@ const DEVICE_NAME_QUERY = ` AND (txs.created >= ud.paired) ` -function batch ( +function batch( from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, @@ -52,20 +54,19 @@ function batch ( status = null, swept = null, excludeTestingCustomers = false, - simplified + simplified, ) { const isCsvExport = _.isBoolean(simplified) const packager = _.flow( _.flatten, _.orderBy(_.property('created'), ['desc']), - _.map(_.flow( - camelize, - _.mapKeys(k => - k == 'cashInFee' ? 'fixedFee' : - k - ) - )), - addProfits + _.map( + _.flow( + camelize, + _.mapKeys(k => (k == 'cashInFee' ? 'fixedFee' : k)), + ), + ), + addProfits, ) const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, @@ -138,46 +139,164 @@ function batch ( let promises if (hasCashInOnlyFilters && hasCashOutOnlyFilters) { - throw new Error('Trying to filter transactions with mutually exclusive filters') + throw new Error( + 'Trying to filter transactions with mutually exclusive filters', + ) } if (hasCashInOnlyFilters) { - promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status])] + promises = [ + db.any(cashInSql, [ + cashInTx.PENDING_INTERVAL, + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + ]), + ] } else if (hasCashOutOnlyFilters) { - promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept])] + promises = [ + db.any(cashOutSql, [ + REDEEMABLE_AGE, + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + ]), + ] } else { promises = [ - db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status]), - db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept]) + db.any(cashInSql, [ + cashInTx.PENDING_INTERVAL, + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + ]), + db.any(cashOutSql, [ + REDEEMABLE_AGE, + from, + until, + limit, + offset, + txClass, + deviceId, + customerName, + fiatCode, + cryptoCode, + toAddress, + status, + swept, + ]), ] } return Promise.all(promises) .then(packager) .then(res => - !isCsvExport ? res : - // GQL transactions and transactionsCsv both use this function and - // if we don't check for the correct simplified value, the Transactions page polling - // will continuously build a csv in the background - simplified ? simplifiedBatch(res) : - advancedBatch(res) + !isCsvExport + ? res + : // GQL transactions and transactionsCsv both use this function and + // if we don't check for the correct simplified value, the Transactions page polling + // will continuously build a csv in the background + simplified + ? simplifiedBatch(res) + : advancedBatch(res), ) } -function advancedBatch (data) { - const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms', - 'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount', - 'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee', - 'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout', - 'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4', - 'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6', - 'denomination1', 'denomination2', 'denomination3', 'denomination4', - 'denominationRecycler1', 'denominationRecycler2', 'denominationRecycler3', 'denominationRecycler4', 'denominationRecycler5', 'denominationRecycler6', - 'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address', - 'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms', - 'discount', 'txHash', 'customerPhone', 'customerEmail', 'customerIdCardDataNumber', - 'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime', - 'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore'] +function advancedBatch(data) { + const fields = [ + 'txClass', + 'id', + 'deviceId', + 'toAddress', + 'cryptoAtoms', + 'cryptoCode', + 'fiat', + 'fiatCode', + 'fee', + 'status', + 'fiatProfit', + 'cryptoAmount', + 'dispense', + 'notified', + 'redeem', + 'phone', + 'error', + 'fixedFee', + 'created', + 'confirmedAt', + 'hdIndex', + 'swept', + 'timedout', + 'dispenseConfirmed', + 'provisioned1', + 'provisioned2', + 'provisioned3', + 'provisioned4', + 'provisionedRecycler1', + 'provisionedRecycler2', + 'provisionedRecycler3', + 'provisionedRecycler4', + 'provisionedRecycler5', + 'provisionedRecycler6', + 'denomination1', + 'denomination2', + 'denomination3', + 'denomination4', + 'denominationRecycler1', + 'denominationRecycler2', + 'denominationRecycler3', + 'denominationRecycler4', + 'denominationRecycler5', + 'denominationRecycler6', + 'errorCode', + 'customerId', + 'txVersion', + 'publishedAt', + 'termsAccepted', + 'layer2Address', + 'commissionPercentage', + 'rawTickerPrice', + 'receivedCryptoAtoms', + 'discount', + 'txHash', + 'customerPhone', + 'customerEmail', + 'customerIdCardDataNumber', + 'customerIdCardDataExpiration', + 'customerIdCardData', + 'customerName', + 'sendTime', + 'customerFrontCameraPath', + 'customerIdCardPhotoPath', + 'expired', + 'machineName', + 'walletScore', + ] const addAdvancedFields = _.map(it => ({ ...it, @@ -191,28 +310,48 @@ function advancedBatch (data) { return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data) } -function simplifiedBatch (data) { - const fields = ['txClass', 'id', 'created', 'machineName', 'fee', - 'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'email', 'toAddress', - 'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount'] +function simplifiedBatch(data) { + const fields = [ + 'txClass', + 'id', + 'created', + 'machineName', + 'fee', + 'cryptoCode', + 'cryptoAtoms', + 'fiat', + 'fiatCode', + 'phone', + 'email', + 'toAddress', + 'txHash', + 'dispense', + 'error', + 'status', + 'fiatProfit', + 'cryptoAmount', + ] const addSimplifiedFields = _.map(it => ({ ...it, status: getStatus(it), fiatProfit: getProfit(it).toString(), - cryptoAmount: getCryptoAmount(it).toString() + cryptoAmount: getCryptoAmount(it).toString(), })) return _.compose(_.map(_.pick(fields)), addSimplifiedFields)(data) } -const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode) +const getCryptoAmount = it => + coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode) const getProfit = it => { /* fiat - crypto*tickerPrice */ - const calcCashInProfit = (fiat, crypto, tickerPrice) => fiat.minus(crypto.times(tickerPrice)) + const calcCashInProfit = (fiat, crypto, tickerPrice) => + fiat.minus(crypto.times(tickerPrice)) /* crypto*tickerPrice - fiat */ - const calcCashOutProfit = (fiat, crypto, tickerPrice) => crypto.times(tickerPrice).minus(fiat) + const calcCashOutProfit = (fiat, crypto, tickerPrice) => + crypto.times(tickerPrice).minus(fiat) const fiat = BN(it.fiat) const crypto = getCryptoAmount(it) @@ -247,10 +386,15 @@ const getStatus = it => { return getCashInStatus(it) } -function getCustomerTransactionsBatch (ids) { - const packager = _.flow(it => { - return it - }, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize)) +function getCustomerTransactionsBatch(ids) { + const packager = _.flow( + it => { + return it + }, + _.flatten, + _.orderBy(_.property('created'), ['desc']), + _.map(camelize), + ) const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, c.phone AS customer_phone, @@ -292,16 +436,25 @@ function getCustomerTransactionsBatch (ids) { WHERE c.id IN ($1^) ORDER BY created DESC limit $2` return Promise.all([ - db.any(cashInSql, [_.map(pgp.as.text, ids).join(','), cashInTx.PENDING_INTERVAL, NUM_RESULTS]), - db.any(cashOutSql, [_.map(pgp.as.text, ids).join(','), NUM_RESULTS, REDEEMABLE_AGE]) + db.any(cashInSql, [ + _.map(pgp.as.text, ids).join(','), + cashInTx.PENDING_INTERVAL, + NUM_RESULTS, + ]), + db.any(cashOutSql, [ + _.map(pgp.as.text, ids).join(','), + NUM_RESULTS, + REDEEMABLE_AGE, + ]), ]) - .then(packager).then(transactions => { + .then(packager) + .then(transactions => { const transactionMap = _.groupBy('customerId', transactions) return ids.map(id => transactionMap[id]) }) } -function single (txId) { +function single(txId) { const packager = _.flow(_.compact, _.map(camelize)) const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*, @@ -344,18 +497,17 @@ function single (txId) { return Promise.all([ db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId]), - db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE]) + db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE]), ]) .then(packager) .then(_.head) } -function cancel (txId) { - return tx.cancel(txId) - .then(() => single(txId)) +function cancel(txId) { + return tx.cancel(txId).then(() => single(txId)) } -function getTx (txId, txClass) { +function getTx(txId, txClass) { const cashInSql = `select 'cashIn' as tx_class, txs.*, ((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired from cash_in_txs as txs @@ -372,7 +524,7 @@ function getTx (txId, txClass) { : db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE]) } -function getTxAssociatedData (txId, txClass) { +function getTxAssociatedData(txId, txClass) { const billsSql = `select 'bills' as bills, b.* from bills b where cash_in_txs_id = $1` const actionsSql = `select 'cash_out_actions' as cash_out_actions, actions.* from cash_out_actions actions where tx_id = $1` @@ -381,15 +533,27 @@ function getTxAssociatedData (txId, txClass) { : db.manyOrNone(actionsSql, [txId]) } -function updateTxCustomerPhoto (customerId, txId, direction, data) { +function updateTxCustomerPhoto(customerId, txId, direction, data) { const formattedData = _.mapKeys(_.snakeCase, data) - const cashInSql = 'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' + const cashInSql = + 'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' - const cashOutSql = 'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' + const cashOutSql = + 'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4' return direction === 'cashIn' - ? db.oneOrNone(cashInSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId]) - : db.oneOrNone(cashOutSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId]) + ? db.oneOrNone(cashInSql, [ + formattedData.tx_customer_photo_at, + formattedData.tx_customer_photo_path, + customerId, + txId, + ]) + : db.oneOrNone(cashOutSql, [ + formattedData.tx_customer_photo_at, + formattedData.tx_customer_photo_path, + customerId, + txId, + ]) } module.exports = { @@ -399,5 +563,5 @@ module.exports = { getCustomerTransactionsBatch, getTx, getTxAssociatedData, - updateTxCustomerPhoto + updateTxCustomerPhoto, } diff --git a/packages/server/lib/new-config-manager.js b/packages/server/lib/new-config-manager.js index 239bfa20..ef4d2070 100644 --- a/packages/server/lib/new-config-manager.js +++ b/packages/server/lib/new-config-manager.js @@ -14,19 +14,26 @@ const namespaces = { CASH_OUT: 'cashOut', CASH_IN: 'cashIn', COMPLIANCE: 'compliance', - MACHINE_SCREENS: 'machineScreens' + MACHINE_SCREENS: 'machineScreens', } const machineScreens = { - RATES: 'rates' + RATES: 'rates', } -const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str) -const filter = namespace => _.pickBy((value, key) => _.startsWith(`${namespace}_`)(key)) +const stripl = _.curry((q, str) => + _.startsWith(q, str) ? str.slice(q.length) : str, +) +const filter = namespace => + _.pickBy((value, key) => _.startsWith(`${namespace}_`)(key)) 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 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) @@ -34,9 +41,12 @@ const getCommissions = (cryptoCode, deviceId, config) => { if (_.isEmpty(overrides)) return _.omit('overrides', commissions) - const specificFilter = it => it.machine === deviceId && _.includes(cryptoCode)(it.cryptoCurrencies) - const specificAllCoinsFilter = it => it.machine === deviceId && _.includes('ALL_COINS')(it.cryptoCurrencies) - const allMachinesFilter = it => it.machine === 'ALL_MACHINES' && _.includes(cryptoCode)(it.cryptoCurrencies) + const specificFilter = it => + it.machine === deviceId && _.includes(cryptoCode)(it.cryptoCurrencies) + const specificAllCoinsFilter = it => + it.machine === deviceId && _.includes('ALL_COINS')(it.cryptoCurrencies) + const allMachinesFilter = it => + it.machine === 'ALL_MACHINES' && _.includes(cryptoCode)(it.cryptoCurrencies) const specificOverrides = _.filter(specificFilter)(overrides) const specificAllCoinsOverrides = _.filter(specificAllCoinsFilter)(overrides) @@ -46,7 +56,7 @@ const getCommissions = (cryptoCode, deviceId, config) => { commissions, ...allMachinesOverrides, ...specificAllCoinsOverrides, - ...specificOverrides + ...specificOverrides, ] return _.omit('overrides', _.assignAll(priorityOrderOverrides)) @@ -55,7 +65,10 @@ 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)])) + return _.omit( + 'overrides', + _.assignAll([locale, ..._.filter(filter)(locale.overrides)]), + ) } const getGlobalLocale = it => getLocale(null, it) @@ -65,22 +78,41 @@ const getWalletSettings = (key, it) => { const getAdvancedSettings = it => { const advancedSettings = fromNamespace(namespaces.ADVANCED)(it) - return _.omit(['overrides', 'cryptoCurrency', 'id'], _.assignAll([advancedSettings, ..._.filter(filter)(advancedSettings.overrides)])) + return _.omit( + ['overrides', 'cryptoCurrency', 'id'], + _.assignAll([ + advancedSettings, + ..._.filter(filter)(advancedSettings.overrides), + ]), + ) } const walletsSettings = fromNamespace(namespaces.WALLETS)(it) - return _.assign(fromNamespace(key)(walletsSettings), getAdvancedSettings(walletsSettings)) + return _.assign( + fromNamespace(key)(walletsSettings), + getAdvancedSettings(walletsSettings), + ) } -const getCashOut = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.CASH_OUT))(it) +const getCashOut = (key, it) => + _.compose(fromNamespace(key), fromNamespace(namespaces.CASH_OUT))(it) const getGlobalCashOut = fromNamespace(namespaces.CASH_OUT) const getOperatorInfo = fromNamespace(namespaces.OPERATOR_INFO) const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR) const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS) const getReceipt = fromNamespace(namespaces.RECEIPT) const getCompliance = fromNamespace(namespaces.COMPLIANCE) -const getMachineScreenOpts = (screenName, config) => _.compose(fromNamespace(screenName), fromNamespace(namespaces.MACHINE_SCREENS))(config) -const getAllMachineScreenOpts = config => _.reduce((acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), {}, _.values(machineScreens)) +const getMachineScreenOpts = (screenName, config) => + _.compose( + fromNamespace(screenName), + fromNamespace(namespaces.MACHINE_SCREENS), + )(config) +const getAllMachineScreenOpts = config => + _.reduce( + (acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), + {}, + _.values(machineScreens), + ) -const getAllCryptoCurrencies = (config) => { +const getAllCryptoCurrencies = config => { const locale = fromNamespace(namespaces.LOCALE)(config) const cryptos = locale.cryptoCurrencies const overridesCryptos = _.map(_.get('cryptoCurrencies'))(locale.overrides) @@ -92,17 +124,38 @@ const getNotifications = (cryptoCurrency, machine, config) => { const smsSettings = fromNamespace('sms', notifications) const emailSettings = fromNamespace('email', notifications) - const notificationCenterSettings = fromNamespace('notificationCenter', notifications) + const notificationCenterSettings = fromNamespace( + 'notificationCenter', + notifications, + ) - const notifNoOverrides = _.omit(['cryptoBalanceOverrides', 'fiatBalanceOverrides'], notifications) + const notifNoOverrides = _.omit( + ['cryptoBalanceOverrides', 'fiatBalanceOverrides'], + notifications, + ) const findByCryptoCurrency = _.find(_.matches({ cryptoCurrency })) const findByMachine = _.find(_.matches({ machine })) - const cryptoFields = ['cryptoHighBalance', 'cryptoLowBalance', 'highBalance', 'lowBalance'] - const fiatFields = ['cashInAlertThreshold', 'fillingPercentageCassette1', 'fillingPercentageCassette2', 'fillingPercentageCassette3', 'fillingPercentageCassette4'] + const cryptoFields = [ + 'cryptoHighBalance', + 'cryptoLowBalance', + 'highBalance', + 'lowBalance', + ] + const fiatFields = [ + 'cashInAlertThreshold', + 'fillingPercentageCassette1', + 'fillingPercentageCassette2', + 'fillingPercentageCassette3', + 'fillingPercentageCassette4', + ] - const getCryptoSettings = _.compose(_.pick(cryptoFields), _.defaultTo(notifications), findByCryptoCurrency) + const getCryptoSettings = _.compose( + _.pick(cryptoFields), + _.defaultTo(notifications), + findByCryptoCurrency, + ) const cryptoSettings = getCryptoSettings(notifications.cryptoBalanceOverrides) if (cryptoSettings.highBalance) { @@ -120,7 +173,14 @@ const getNotifications = (cryptoCurrency, machine, config) => { _.assignWith(_.defaultTo, notifications), _.pick(fiatFields), )(notifications.fiatBalanceOverrides) - return { ...notifNoOverrides, sms: smsSettings, email: emailSettings, ...cryptoSettings, ...fiatSettings, notificationCenter: notificationCenterSettings } + return { + ...notifNoOverrides, + sms: smsSettings, + email: emailSettings, + ...cryptoSettings, + ...fiatSettings, + notificationCenter: notificationCenterSettings, + } } const getGlobalNotifications = config => getNotifications(null, null, config) @@ -132,53 +192,77 @@ function getCustomerAuthenticationMethod(config) { } /* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */ -const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => { - return customInfoRequests - .then(infoRequests => { - const defaultAutomation = _.get('triggersConfig_automation')(config) - const overrides = _.get('triggersConfig_overrides')(config) +const getTriggersAutomation = ( + customInfoRequests, + config, + oldFormat = false, +) => { + return customInfoRequests.then(infoRequests => { + const defaultAutomation = _.get('triggersConfig_automation')(config) + const overrides = _.get('triggersConfig_overrides')(config) - const requirements = { - sanctions: defaultAutomation, - idCardPhoto: defaultAutomation, - idCardData: defaultAutomation, - facephoto: defaultAutomation, - usSsn: defaultAutomation - } - - if (oldFormat) { - _.forEach(it => { requirements[it.id] = defaultAutomation }, infoRequests) - const oldRequirementsOverrides = _.reduce((acc, override) => _.assign(acc, { [override.requirement]: override.automation }), {}, overrides) - return _.assign(requirements, oldRequirementsOverrides) - } - - requirements.custom = [] + const requirements = { + sanctions: defaultAutomation, + idCardPhoto: defaultAutomation, + idCardData: defaultAutomation, + facephoto: defaultAutomation, + usSsn: defaultAutomation, + } + if (oldFormat) { _.forEach(it => { - requirements.custom.push({ id: it.id, type: defaultAutomation }) + requirements[it.id] = defaultAutomation }, infoRequests) + const oldRequirementsOverrides = _.reduce( + (acc, override) => + _.assign(acc, { [override.requirement]: override.automation }), + {}, + overrides, + ) + return _.assign(requirements, oldRequirementsOverrides) + } - const requirementsOverrides = _.reduce((acc, override) => { + requirements.custom = [] + + _.forEach(it => { + requirements.custom.push({ id: it.id, type: defaultAutomation }) + }, infoRequests) + + const requirementsOverrides = _.reduce( + (acc, override) => { return _.assign( acc, !validate(override.requirement) ? { [override.requirement]: override.automation } - : { custom: [...acc.custom ?? [], { id: override.requirement, type: override.automation }] } + : { + custom: [ + ...(acc.custom ?? []), + { id: override.requirement, type: override.automation }, + ], + }, ) - }, {}, overrides) + }, + {}, + overrides, + ) - return _.assign(requirements, requirementsOverrides) - }) + return _.assign(requirements, requirementsOverrides) + }) } const splitGetFirst = _.compose(_.head, _.split('_')) -const getCryptosFromWalletNamespace = - _.compose(_.without(['advanced']), _.uniq, _.map(splitGetFirst), _.keys, fromNamespace('wallets')) +const getCryptosFromWalletNamespace = _.compose( + _.without(['advanced']), + _.uniq, + _.map(splitGetFirst), + _.keys, + fromNamespace('wallets'), +) const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config) -const getCryptoUnits = (crypto, config) => +const getCryptoUnits = (crypto, config) => getWalletSettings(crypto, config).cryptoUnits ?? 'full' const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS) diff --git a/packages/server/lib/new-settings-loader.js b/packages/server/lib/new-settings-loader.js index 5b4dc0a7..090d80ba 100644 --- a/packages/server/lib/new-settings-loader.js +++ b/packages/server/lib/new-settings-loader.js @@ -3,7 +3,10 @@ const crypto = require('crypto') const _ = require('lodash/fp') const db = require('./db') const { getOperatorId } = require('./operator') -const { getTermsConditions, setTermsConditions } = require('./new-config-manager') +const { + getTermsConditions, + setTermsConditions, +} = require('./new-config-manager') const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2 const PASSWORD_FILLED = 'PASSWORD_FILLED' @@ -27,39 +30,32 @@ const SECRET_FIELDS = [ 'galoy.apiSecret', 'bitfinex.secret', 'sumsub.apiToken', - 'sumsub.privateKey' + 'sumsub.privateKey', ] /* * JSON.stringify isn't necessarily deterministic so this function may compute * different hashes for the same object. */ -const md5hash = text => - crypto - .createHash('MD5') - .update(text) - .digest('hex') +const md5hash = text => crypto.createHash('MD5').update(text).digest('hex') const addTermsHash = configs => { const terms = _.omit(['hash'], getTermsConditions(configs)) - return !terms?.text ? - configs : - _.flow( - _.get('text'), - md5hash, - hash => _.set('hash', hash, terms), - setTermsConditions, - _.assign(configs), - )(terms) + return !terms?.text + ? configs + : _.flow( + _.get('text'), + md5hash, + hash => _.set('hash', hash, terms), + setTermsConditions, + _.assign(configs), + )(terms) } const notifyReload = (dbOrTx, operatorId) => - dbOrTx.none( - 'NOTIFY $1:name, $2', - ['reload', JSON.stringify({ operatorId })] - ) + dbOrTx.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ operatorId })]) -function saveAccounts (accounts) { +function saveAccounts(accounts) { if (!accounts) { return Promise.resolve() } @@ -68,8 +64,8 @@ function saveAccounts (accounts) { INSERT INTO user_config (type, data, valid, schema_version) SELECT 'accounts', $1, TRUE, $2 WHERE 'accounts' NOT IN (SELECT type FROM user_config)` - return Promise.all([loadAccounts(), getOperatorId('middleware')]) - .then(([currentAccounts, operatorId]) => { + return Promise.all([loadAccounts(), getOperatorId('middleware')]).then( + ([currentAccounts, operatorId]) => { const newAccounts = _.merge(currentAccounts, accounts) // Only allow one wallet scoring active at a time @@ -81,14 +77,21 @@ function saveAccounts (accounts) { newAccounts.elliptic.enabled = false } - return db.tx(t => - t.none(accountsSql, [{ accounts: newAccounts }, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) - .then(() => notifyReload(t, operatorId)) - ).catch(console.error) - }) + return db + .tx(t => + t + .none(accountsSql, [ + { accounts: newAccounts }, + NEW_SETTINGS_LOADER_SCHEMA_VERSION, + ]) + .then(() => notifyReload(t, operatorId)), + ) + .catch(console.error) + }, + ) } -function loadAccounts (schemaVersion) { +function loadAccounts(schemaVersion) { const sql = `SELECT data FROM user_config WHERE type = $1 @@ -97,72 +100,85 @@ function loadAccounts (schemaVersion) { ORDER BY id DESC LIMIT 1` - return db.oneOrNone(sql, ['accounts', schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + return db + .oneOrNone(sql, [ + 'accounts', + schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION, + ]) .then(_.compose(_.defaultTo({}), _.get('data.accounts'))) } -function hideSecretFields (accounts) { +function hideSecretFields(accounts) { return _.flow( _.filter(path => !_.isEmpty(_.get(path, accounts))), _.reduce( (accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts), - accounts - ) + accounts, + ), )(SECRET_FIELDS) } -function showAccounts (schemaVersion) { - return loadAccounts(schemaVersion) - .then(hideSecretFields) +function showAccounts(schemaVersion) { + return loadAccounts(schemaVersion).then(hideSecretFields) } const insertConfigRow = (dbOrTx, data) => dbOrTx.none( "INSERT INTO user_config (type, data, valid, schema_version) VALUES ('config', $1, TRUE, $2)", - [data, NEW_SETTINGS_LOADER_SCHEMA_VERSION] + [data, NEW_SETTINGS_LOADER_SCHEMA_VERSION], ) -function saveConfig (config) { - return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')]) - .then(([currentConfig, operatorId]) => { - const newConfig = addTermsHash(_.assign(currentConfig, config)) - return db.tx(t => - insertConfigRow(t, { config: newConfig }) - .then(() => notifyReload(t, operatorId)) - ).catch(console.error) - }) +function saveConfig(config) { + return Promise.all([ + loadLatestConfigOrNone(), + getOperatorId('middleware'), + ]).then(([currentConfig, operatorId]) => { + const newConfig = addTermsHash(_.assign(currentConfig, config)) + return db + .tx(t => + insertConfigRow(t, { config: newConfig }).then(() => + notifyReload(t, operatorId), + ), + ) + .catch(console.error) + }) } -function removeFromConfig (fields) { - return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')]) - .then(([currentConfig, operatorId]) => { - const newConfig = _.omit(fields, currentConfig) - return db.tx(t => - insertConfigRow(t, { config: newConfig }) - .then(() => notifyReload(t, operatorId)) - ).catch(console.error) - }) +function removeFromConfig(fields) { + return Promise.all([ + loadLatestConfigOrNone(), + getOperatorId('middleware'), + ]).then(([currentConfig, operatorId]) => { + const newConfig = _.omit(fields, currentConfig) + return db + .tx(t => + insertConfigRow(t, { config: newConfig }).then(() => + notifyReload(t, operatorId), + ), + ) + .catch(console.error) + }) } -function migrationSaveConfig (config) { - return loadLatestConfigOrNone() - .then(currentConfig => { - const newConfig = _.assign(currentConfig, config) - return insertConfigRow(db, { config: newConfig }) - .catch(console.error) - }) +function migrationSaveConfig(config) { + return loadLatestConfigOrNone().then(currentConfig => { + const newConfig = _.assign(currentConfig, config) + return insertConfigRow(db, { config: newConfig }).catch(console.error) + }) } -function loadLatest (schemaVersion) { - return Promise.all([loadLatestConfigOrNoneReturningVersion(schemaVersion), loadAccounts(schemaVersion)]) - .then(([configObj, accounts]) => ({ - config: configObj.config, - accounts, - version: configObj.version - })) +function loadLatest(schemaVersion) { + return Promise.all([ + loadLatestConfigOrNoneReturningVersion(schemaVersion), + loadAccounts(schemaVersion), + ]).then(([configObj, accounts]) => ({ + config: configObj.config, + accounts, + version: configObj.version, + })) } -function loadLatestConfig () { +function loadLatestConfig() { const sql = `SELECT data FROM user_config WHERE type = 'config' @@ -170,15 +186,16 @@ function loadLatestConfig () { AND valid ORDER BY id DESC LIMIT 1` - - return db.oneOrNone(sql, [NEW_SETTINGS_LOADER_SCHEMA_VERSION]) - .then(row => row ? row.data.config : {}) + + return db + .oneOrNone(sql, [NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + .then(row => (row ? row.data.config : {})) .catch(err => { throw err }) } -function loadLatestConfigOrNoneReturningVersion (schemaVersion) { +function loadLatestConfigOrNoneReturningVersion(schemaVersion) { const sql = `SELECT data, id FROM user_config WHERE type = 'config' @@ -187,23 +204,25 @@ function loadLatestConfigOrNoneReturningVersion (schemaVersion) { ORDER BY id DESC LIMIT 1` - return db.oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) - .then(row => row ? { config: row.data.config, version: row.id } : {}) + return db + .oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + .then(row => (row ? { config: row.data.config, version: row.id } : {})) } -function loadLatestConfigOrNone (schemaVersion) { +function loadLatestConfigOrNone(schemaVersion) { const sql = `SELECT data FROM user_config WHERE type = 'config' AND schema_version = $1 ORDER BY id DESC LIMIT 1` - - return db.oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) - .then(row => row ? row.data.config : {}) + + return db + .oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + .then(row => (row ? row.data.config : {})) } -function loadConfig (versionId) { +function loadConfig(versionId) { const sql = `SELECT data FROM user_config WHERE id = $1 @@ -211,7 +230,8 @@ function loadConfig (versionId) { AND schema_version = $2 AND valid` - return db.one(sql, [versionId, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + return db + .one(sql, [versionId, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row.data.config) .catch(err => { if (err.name === 'QueryResultError') { @@ -222,14 +242,15 @@ function loadConfig (versionId) { }) } -function load (versionId) { +function load(versionId) { if (!versionId) Promise.reject('versionId is required') - - return Promise.all([loadConfig(versionId), loadAccounts()]) - .then(([config, accounts]) => ({ + + return Promise.all([loadConfig(versionId), loadAccounts()]).then( + ([config, accounts]) => ({ config, - accounts - })) + accounts, + }), + ) } const fetchCurrentConfigVersion = () => { diff --git a/packages/server/lib/notifier/codes.js b/packages/server/lib/notifier/codes.js index b0e02895..3ff31b2e 100644 --- a/packages/server/lib/notifier/codes.js +++ b/packages/server/lib/notifier/codes.js @@ -18,7 +18,7 @@ const CODES_DISPLAY = { LOW_CASH_OUT: 'Low Cash-out', LOW_RECYCLER_STACKER: 'Low Recycler Stacker', HIGH_RECYCLER_STACKER: 'High Recycler Stacker', - CASHBOX_REMOVED: 'Cashbox removed' + CASHBOX_REMOVED: 'Cashbox removed', } const NETWORK_DOWN_TIME = 3 * T.minute @@ -32,7 +32,7 @@ const NOTIFICATION_TYPES = { CRYPTO_BALANCE: 'cryptoBalance', COMPLIANCE: 'compliance', ERROR: 'error', - SECURITY: 'security' + SECURITY: 'security', } module.exports = { @@ -48,5 +48,5 @@ module.exports = { NETWORK_DOWN_TIME, STALE_STATE, ALERT_SEND_INTERVAL, - NOTIFICATION_TYPES + NOTIFICATION_TYPES, } diff --git a/packages/server/lib/notifier/email.js b/packages/server/lib/notifier/email.js index c53df248..e886aa8c 100644 --- a/packages/server/lib/notifier/email.js +++ b/packages/server/lib/notifier/email.js @@ -11,10 +11,10 @@ const { CASH_BOX_FULL, LOW_CASH_OUT, LOW_RECYCLER_STACKER, - SECURITY + SECURITY, } = require('./codes') -function alertSubject (alertRec, config) { +function alertSubject(alertRec, config) { let alerts = [] if (config.balance) { @@ -27,11 +27,16 @@ function alertSubject (alertRec, config) { if (alerts.length === 0) return null - const alertTypes = _.flow(_.map('code'), _.uniq, _.map(utils.codeDisplay), _.sortBy(o => o))(alerts) + const alertTypes = _.flow( + _.map('code'), + _.uniq, + _.map(utils.codeDisplay), + _.sortBy(o => o), + )(alerts) return '[Lamassu] Errors reported: ' + alertTypes.join(', ') } -function printEmailAlerts (alertRec, config) { +function printEmailAlerts(alertRec, config) { let body = 'Errors were reported by your Lamassu Machines.\n' if (config.balance && alertRec.general.length !== 0) { @@ -50,30 +55,39 @@ function printEmailAlerts (alertRec, config) { return body } -function emailAlerts (alerts) { +function emailAlerts(alerts) { return _.join('\n', _.map(emailAlert, alerts)) + '\n' } -function emailAlert (alert) { +function emailAlert(alert) { switch (alert.code) { case PING: if (alert.age) { - const pingAge = utils.formatAge(alert.age, { compact: true, verbose: true }) + const pingAge = utils.formatAge(alert.age, { + compact: true, + verbose: true, + }) return `Machine down for ${pingAge}` } return 'Machine down for a while.' case STALE: { - const stuckAge = utils.formatAge(alert.age, { compact: true, verbose: true }) + const stuckAge = utils.formatAge(alert.age, { + compact: true, + verbose: true, + }) return `Machine is stuck on ${alert.state} screen for ${stuckAge}` } case LOW_CRYPTO_BALANCE: { - const balance = utils.formatCurrency(alert.fiatBalance.balance, alert.fiatCode) + const balance = utils.formatCurrency( + alert.fiatBalance.balance, + alert.fiatCode, + ) return `Low balance in ${alert.cryptoCode} [${balance}]` } case HIGH_CRYPTO_BALANCE: { const highBalance = utils.formatCurrency( alert.fiatBalance.balance, - alert.fiatCode + alert.fiatCode, ) return `High balance in ${alert.cryptoCode} [${highBalance}]` } diff --git a/packages/server/lib/notifier/index.js b/packages/server/lib/notifier/index.js index 8e61f162..a45ac7d4 100644 --- a/packages/server/lib/notifier/index.js +++ b/packages/server/lib/notifier/index.js @@ -13,48 +13,52 @@ const smsFuncs = require('./sms') const webhookFuncs = require('./webhook') const { STALE, STALE_STATE } = require('./codes') -function buildMessage (alerts, notifications) { +function buildMessage(alerts, notifications) { const smsEnabled = utils.isActive(notifications.sms) const emailEnabled = utils.isActive(notifications.email) let rec = {} if (smsEnabled) { rec = _.set(['sms', 'body'])( - smsFuncs.printSmsAlerts(alerts, notifications.sms) + smsFuncs.printSmsAlerts(alerts, notifications.sms), )(rec) } if (emailEnabled) { rec = _.set(['email', 'subject'])( - emailFuncs.alertSubject(alerts, notifications.email) + emailFuncs.alertSubject(alerts, notifications.email), )(rec) rec = _.set(['email', 'body'])( - emailFuncs.printEmailAlerts(alerts, notifications.email) + emailFuncs.printEmailAlerts(alerts, notifications.email), )(rec) } return rec } -function checkNotification (plugins) { +function checkNotification(plugins) { const notifications = plugins.getNotificationConfig() const smsEnabled = utils.isActive(notifications.sms) const emailEnabled = utils.isActive(notifications.email) - const notificationCenterEnabled = utils.isActive(notifications.notificationCenter) + const notificationCenterEnabled = utils.isActive( + notifications.notificationCenter, + ) - if (!(notificationCenterEnabled || smsEnabled || emailEnabled)) return Promise.resolve() + if (!(notificationCenterEnabled || smsEnabled || emailEnabled)) + return Promise.resolve() return getAlerts(plugins) .then(alerts => { notifyIfActive('errors', 'errorAlertsNotify', alerts) const currentAlertFingerprint = utils.buildAlertFingerprint( alerts, - notifications + notifications, ) if (!currentAlertFingerprint) { const inAlert = !!utils.getAlertFingerprint() // variables for setAlertFingerprint: (fingerprint = null, lastAlertTime = null) utils.setAlertFingerprint(null, null) - if (inAlert) return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled) + if (inAlert) + return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled) } if (utils.shouldNotAlert(currentAlertFingerprint)) return @@ -70,18 +74,18 @@ function checkNotification (plugins) { .catch(logger.error) } -function getAlerts (plugins) { +function getAlerts(plugins) { return Promise.all([ plugins.checkBalances(), queries.machineEvents(), - plugins.getMachineNames() + plugins.getMachineNames(), ]).then(([balances, events, devices]) => { notifyIfActive('balance', 'balancesNotify', balances) return buildAlerts(checkPings(devices), balances, events, devices) }) } -function buildAlerts (pings, balances, events, devices) { +function buildAlerts(pings, balances, events, devices) { const alerts = { devices: {}, deviceNames: {} } alerts.general = _.filter(r => !r.deviceId, balances) _.forEach(device => { @@ -89,10 +93,11 @@ function buildAlerts (pings, balances, events, devices) { const ping = pings[deviceId] || [] const stuckScreen = checkStuckScreen(events, device) - alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter( - ['deviceId', deviceId], - balances - ), alerts.devices) + alerts.devices = _.set( + [deviceId, 'balanceAlerts'], + _.filter(['deviceId', deviceId], balances), + alerts.devices, + ) alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping alerts.deviceNames[deviceId] = device.name @@ -101,18 +106,18 @@ function buildAlerts (pings, balances, events, devices) { return alerts } -function checkPings (devices) { +function checkPings(devices) { const deviceIds = _.map('deviceId', devices) const pings = _.map(utils.checkPing, devices) return _.zipObject(deviceIds)(pings) } -function checkStuckScreen (deviceEvents, machine) { +function checkStuckScreen(deviceEvents, machine) { const lastEvent = _.pipe( _.filter(e => e.device_id === machine.deviceId), _.sortBy(utils.getDeviceTime), _.map(utils.parseEventNote), - _.last + _.last, )(deviceEvents) if (!lastEvent) return [] @@ -129,115 +134,152 @@ function checkStuckScreen (deviceEvents, machine) { return [] } -function transactionNotify (tx, rec) { +function transactionNotify(tx, rec) { return settingsLoader.loadLatestConfig().then(config => { const notifSettings = configManager.getGlobalNotifications(config) - const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity) + const highValueTx = tx.fiat.gt( + notifSettings.highValueTransaction || Infinity, + ) const isCashOut = tx.direction === 'cashOut' // for notification center const directionDisplay = isCashOut ? 'cash-out' : 'cash-in' - const readyToNotify = !isCashOut || (tx.direction === 'cashOut' && rec.isRedemption) + const readyToNotify = + !isCashOut || (tx.direction === 'cashOut' && rec.isRedemption) // awaiting for redesign. notification should not be sent if toggle in the settings table is disabled, // but currently we're sending notifications of high value tx even with the toggle disabled if (readyToNotify && !highValueTx) { - notifyIfActive('transactions', 'notifCenterTransactionNotify', highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress) + notifyIfActive( + 'transactions', + 'notifCenterTransactionNotify', + highValueTx, + directionDisplay, + tx.fiat, + tx.fiatCode, + tx.deviceId, + tx.toAddress, + ) } else if (readyToNotify && highValueTx) { - notificationCenter.notifCenterTransactionNotify(highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress) + notificationCenter.notifCenterTransactionNotify( + highValueTx, + directionDisplay, + tx.fiat, + tx.fiatCode, + tx.deviceId, + tx.toAddress, + ) } // alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled - const walletSettings = configManager.getWalletSettings(tx.cryptoCode, config) + const walletSettings = configManager.getWalletSettings( + tx.cryptoCode, + config, + ) const zeroConfLimit = walletSettings.zeroConfLimit || 0 const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit) - const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions - const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({}) + const notificationsEnabled = + notifSettings.sms.transactions || notifSettings.email.transactions + const customerPromise = tx.customerId + ? customers.getById(tx.customerId) + : Promise.resolve({}) if (!notificationsEnabled && !highValueTx) return Promise.resolve() - if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve() - if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error) + if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) + return Promise.resolve() + if (!zeroConf && rec.isRedemption) + return sendRedemptionMessage(tx.id, rec.error) - return Promise.all([ - queries.getMachineName(tx.deviceId), - customerPromise - ]).then(([machineName, customer]) => { - return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer) - }).then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx)) + return Promise.all([queries.getMachineName(tx.deviceId), customerPromise]) + .then(([machineName, customer]) => { + return utils.buildTransactionMessage( + tx, + rec, + highValueTx, + machineName, + customer, + ) + }) + .then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx)) }) } -function complianceNotify (settings, customer, deviceId, action, period) { - const timestamp = (new Date()).toLocaleString() - return queries.getMachineName(deviceId) - .then(machineName => { - const notifications = configManager.getGlobalNotifications(settings.config) +function complianceNotify(settings, customer, deviceId, action, period) { + const timestamp = new Date().toLocaleString() + return queries.getMachineName(deviceId).then(machineName => { + const notifications = configManager.getGlobalNotifications(settings.config) - const msgCore = { - BLOCKED: `was blocked`, - SUSPENDED: `was suspended for ${!!period && period} days`, - PENDING_COMPLIANCE: `is waiting for your manual approval`, - } + const msgCore = { + BLOCKED: `was blocked`, + SUSPENDED: `was suspended for ${!!period && period} days`, + PENDING_COMPLIANCE: `is waiting for your manual approval`, + } - const rec = { - sms: { - body: `Customer ${customer.phone} ${msgCore[action]} - ${machineName}. ${timestamp}` - }, - email: { - subject: `Customer compliance`, - body: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}` - }, - webhook: { - topic: `Customer compliance`, - content: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}` - } - } + const rec = { + sms: { + body: `Customer ${customer.phone} ${msgCore[action]} - ${machineName}. ${timestamp}`, + }, + email: { + subject: `Customer compliance`, + body: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}`, + }, + webhook: { + topic: `Customer compliance`, + content: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}`, + }, + } - const promises = [] + const promises = [] - const emailActive = - notifications.email.active && - notifications.email.compliance + const emailActive = + notifications.email.active && notifications.email.compliance - const smsActive = - notifications.sms.active && - notifications.sms.compliance + const smsActive = notifications.sms.active && notifications.sms.compliance - const webhookActive = true + const webhookActive = true - if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec)) - if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec)) - if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec)) + if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec)) + if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec)) + if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec)) - notifyIfActive('compliance', 'customerComplianceNotify', customer, deviceId, action, machineName, period) + notifyIfActive( + 'compliance', + 'customerComplianceNotify', + customer, + deviceId, + action, + machineName, + period, + ) - return Promise.all(promises) - .catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`)) - }) + return Promise.all(promises).catch(err => + console.error( + `An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`, + ), + ) + }) } -function sendRedemptionMessage (txId, error) { +function sendRedemptionMessage(txId, error) { const subject = `Here's an update on transaction ${txId}` - const body = error - ? `Error: ${error}` - : 'It was just dispensed successfully' + const body = error ? `Error: ${error}` : 'It was just dispensed successfully' const rec = { sms: { - body: `${subject} - ${body}` + body: `${subject} - ${body}`, }, email: { subject, - body + body, }, webhook: { topic: `Transaction update`, - content: body - } + content: body, + }, } return sendTransactionMessage(rec) } -function sendTransactionMessage (rec, isHighValueTx) { +function sendTransactionMessage(rec, isHighValueTx) { return settingsLoader.loadLatest().then(settings => { const notifications = configManager.getGlobalNotifications(settings.config) @@ -258,62 +300,74 @@ function sendTransactionMessage (rec, isHighValueTx) { const webhookActive = true if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec)) - return Promise.all(promises) - .catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`)) + return Promise.all(promises).catch(err => + console.error( + `An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`, + ), + ) }) } -function cashboxNotify (deviceId) { +function cashboxNotify(deviceId) { return Promise.all([ settingsLoader.loadLatest(), - queries.getMachineName(deviceId) - ]) - .then(([settings, machineName]) => { - const notifications = configManager.getGlobalNotifications(settings.config) - const rec = { - sms: { - body: `Cashbox removed - ${machineName}` - }, - email: { - subject: `Cashbox removal`, - body: `Cashbox removed in machine ${machineName}` - }, - webhook: { - topic: `Cashbox removal`, - content: `Cashbox removed in machine ${machineName}` - } - } + queries.getMachineName(deviceId), + ]).then(([settings, machineName]) => { + const notifications = configManager.getGlobalNotifications(settings.config) + const rec = { + sms: { + body: `Cashbox removed - ${machineName}`, + }, + email: { + subject: `Cashbox removal`, + body: `Cashbox removed in machine ${machineName}`, + }, + webhook: { + topic: `Cashbox removal`, + content: `Cashbox removed in machine ${machineName}`, + }, + } - const promises = [] + const promises = [] - const emailActive = - notifications.email.active && - notifications.email.security + const emailActive = + notifications.email.active && notifications.email.security - const smsActive = - notifications.sms.active && - notifications.sms.security + const smsActive = notifications.sms.active && notifications.sms.security - const webhookActive = true - - if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec)) - if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec)) - if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec)) - notifyIfActive('security', 'cashboxNotify', deviceId) + const webhookActive = true - return Promise.all(promises) - .catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`)) - }) + if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec)) + if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec)) + if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec)) + notifyIfActive('security', 'cashboxNotify', deviceId) + + return Promise.all(promises).catch(err => + console.error( + `An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`, + ), + ) + }) } // for notification center, check if type of notification is active before calling the respective notify function const notifyIfActive = (type, fnName, ...args) => { - return settingsLoader.loadLatestConfig().then(config => { - const notificationSettings = configManager.getGlobalNotifications(config).notificationCenter - if (!notificationCenter[fnName]) return Promise.reject(new Error(`Notification function ${fnName} for type ${type} does not exist`)) - if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve() - return notificationCenter[fnName](...args) - }).catch(logger.error) + return settingsLoader + .loadLatestConfig() + .then(config => { + const notificationSettings = + configManager.getGlobalNotifications(config).notificationCenter + if (!notificationCenter[fnName]) + return Promise.reject( + new Error( + `Notification function ${fnName} for type ${type} does not exist`, + ), + ) + if (!(notificationSettings.active && notificationSettings[type])) + return Promise.resolve() + return notificationCenter[fnName](...args) + }) + .catch(logger.error) } module.exports = { @@ -324,5 +378,5 @@ module.exports = { checkStuckScreen, sendRedemptionMessage, cashboxNotify, - notifyIfActive + notifyIfActive, } diff --git a/packages/server/lib/notifier/notificationCenter.js b/packages/server/lib/notifier/notificationCenter.js index 7f3480b0..f77817fa 100644 --- a/packages/server/lib/notifier/notificationCenter.js +++ b/packages/server/lib/notifier/notificationCenter.js @@ -11,7 +11,7 @@ const { FIAT_BALANCE, ERROR, HIGH_VALUE_TX, - NORMAL_VALUE_TX + NORMAL_VALUE_TX, }, STALE, @@ -28,9 +28,15 @@ const sanctionsNotify = (customer, phone) => { const code = 'SANCTIONS' const detailB = utils.buildDetail({ customerId: customer.id, code }) const addNotif = phone => - queries.addNotification(COMPLIANCE, `Blocked customer with phone ${phone} for being on the OFAC sanctions list`, detailB) + queries.addNotification( + COMPLIANCE, + `Blocked customer with phone ${phone} for being on the OFAC sanctions list`, + detailB, + ) // if it's a new customer then phone comes as undefined - return phone ? addNotif(phone) : customers.getById(customer.id).then(c => addNotif(c.phone)) + return phone + ? addNotif(phone) + : customers.getById(customer.id).then(c => addNotif(c.phone)) } const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => { @@ -38,16 +44,25 @@ const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => { return queries.invalidateNotification(detailB, 'compliance') } -const customerComplianceNotify = (customer, deviceId, code, machineName, days = null) => { +const customerComplianceNotify = ( + customer, + deviceId, + code, + machineName, + days = null, +) => { // code for now can be "BLOCKED", "SUSPENDED" const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId }) const date = new Date() if (days) { date.setDate(date.getDate() + days) } - const message = code === 'SUSPENDED' ? `Customer ${customer.phone} suspended until ${date.toLocaleString()}` : - code === 'BLOCKED' ? `Customer ${customer.phone} blocked` : - `Customer ${customer.phone} has pending compliance in machine ${machineName}` + const message = + code === 'SUSPENDED' + ? `Customer ${customer.phone} suspended until ${date.toLocaleString()}` + : code === 'BLOCKED' + ? `Customer ${customer.phone} blocked` + : `Customer ${customer.phone} has pending compliance in machine ${machineName}` return clearOldCustomerSuspendedNotifications(customer.id, deviceId) .then(() => queries.getValidNotifications(COMPLIANCE, detailB)) @@ -57,35 +72,52 @@ const customerComplianceNotify = (customer, deviceId, code, machineName, days = }) } -const clearOldFiatNotifications = (balances) => { +const clearOldFiatNotifications = balances => { return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => { const filterByBalance = _.filter(notification => { const { cassette, deviceId } = notification.detail - return !_.find(balance => balance.cassette === cassette && balance.deviceId === deviceId)(balances) + return !_.find( + balance => + balance.cassette === cassette && balance.deviceId === deviceId, + )(balances) }) - const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(notifications) + const indexesToInvalidate = _.compose( + _.map('id'), + filterByBalance, + )(notifications) const notInvalidated = _.filter(notification => { return !_.find(id => notification.id === id)(indexesToInvalidate) }, notifications) - return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) + return ( + indexesToInvalidate.length + ? queries.batchInvalidate(indexesToInvalidate) + : Promise.resolve() + ).then(() => notInvalidated) }) } -const fiatBalancesNotify = (fiatWarnings) => { +const fiatBalancesNotify = fiatWarnings => { return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => { return fiatWarnings.forEach(balance => { - if (_.find(o => { - const { cassette, deviceId } = o.detail - return cassette === balance.cassette && deviceId === balance.deviceId - }, notInvalidated)) return - const message = balance.code === LOW_CASH_OUT ? - `Cash-out cassette ${balance.cassette} low or empty!` : - balance.code === LOW_RECYCLER_STACKER ? - `Recycler ${balance.cassette} low or empty!` : - balance.code === CASH_BOX_FULL ? - `Cash box full or almost full!` : - `Cash box full or almost full!` /* Shouldn't happen */ - const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette }) + if ( + _.find(o => { + const { cassette, deviceId } = o.detail + return cassette === balance.cassette && deviceId === balance.deviceId + }, notInvalidated) + ) + return + const message = + balance.code === LOW_CASH_OUT + ? `Cash-out cassette ${balance.cassette} low or empty!` + : balance.code === LOW_RECYCLER_STACKER + ? `Recycler ${balance.cassette} low or empty!` + : balance.code === CASH_BOX_FULL + ? `Cash box full or almost full!` + : `Cash box full or almost full!` /* Shouldn't happen */ + const detailB = utils.buildDetail({ + deviceId: balance.deviceId, + cassette: balance.cassette, + }) return queries.addNotification(FIAT_BALANCE, message, detailB) }) }) @@ -95,82 +127,112 @@ const clearOldCryptoNotifications = balances => { return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => { const filterByBalance = _.filter(notification => { const { cryptoCode, code } = notification.detail - return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances) + return !_.find( + balance => balance.cryptoCode === cryptoCode && balance.code === code, + )(balances) }) const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res) const notInvalidated = _.filter(notification => { return !_.find(id => notification.id === id)(indexesToInvalidate) }, res) - return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) + return ( + indexesToInvalidate.length + ? queries.batchInvalidate(indexesToInvalidate) + : Promise.resolve() + ).then(() => notInvalidated) }) } -const cryptoBalancesNotify = (cryptoWarnings) => { +const cryptoBalancesNotify = cryptoWarnings => { return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => { return cryptoWarnings.forEach(balance => { // if notification exists in DB and wasnt invalidated then don't add a duplicate - if (_.find(o => { - const { code, cryptoCode } = o.detail - return code === balance.code && cryptoCode === balance.cryptoCode - }, notInvalidated)) return + if ( + _.find(o => { + const { code, cryptoCode } = o.detail + return code === balance.code && cryptoCode === balance.cryptoCode + }, notInvalidated) + ) + return - const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode) + const fiat = utils.formatCurrency( + balance.fiatBalance.balance, + balance.fiatCode, + ) const message = `${balance.code === HIGH_CRYPTO_BALANCE ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]` - const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code }) + const detailB = utils.buildDetail({ + cryptoCode: balance.cryptoCode, + code: balance.code, + }) return queries.addNotification(CRYPTO_BALANCE, message, detailB) }) }) } -const balancesNotify = (balances) => { - const isCryptoCode = c => _.includes(c, [HIGH_CRYPTO_BALANCE, LOW_CRYPTO_BALANCE]) - const isFiatCode = c => _.includes(c, [LOW_CASH_OUT, CASH_BOX_FULL, LOW_RECYCLER_STACKER]) +const balancesNotify = balances => { + const isCryptoCode = c => + _.includes(c, [HIGH_CRYPTO_BALANCE, LOW_CRYPTO_BALANCE]) + const isFiatCode = c => + _.includes(c, [LOW_CASH_OUT, CASH_BOX_FULL, LOW_RECYCLER_STACKER]) const by = o => - isCryptoCode(o) ? 'crypto' : - isFiatCode(o) ? 'fiat' : - undefined + isCryptoCode(o) ? 'crypto' : isFiatCode(o) ? 'fiat' : undefined const warnings = _.flow( _.groupBy(_.flow(_.get(['code']), by)), _.update('crypto', _.defaultTo([])), _.update('fiat', _.defaultTo([])), )(balances) - return Promise.all([cryptoBalancesNotify(warnings.crypto), fiatBalancesNotify(warnings.fiat)]) + return Promise.all([ + cryptoBalancesNotify(warnings.crypto), + fiatBalancesNotify(warnings.fiat), + ]) } const clearOldErrorNotifications = alerts => { - return queries.getAllValidNotifications(ERROR) - .then(res => { - // for each valid notification in DB see if it exists in alerts - // if the notification doesn't exist in alerts, it is not valid anymore - const filterByAlert = _.filter(notification => { - const { code, deviceId } = notification.detail - return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts) - }) - const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res) - if (!indexesToInvalidate.length) return Promise.resolve() - return queries.batchInvalidate(indexesToInvalidate) + return queries.getAllValidNotifications(ERROR).then(res => { + // for each valid notification in DB see if it exists in alerts + // if the notification doesn't exist in alerts, it is not valid anymore + const filterByAlert = _.filter(notification => { + const { code, deviceId } = notification.detail + return !_.find( + alert => alert.code === code && alert.deviceId === deviceId, + )(alerts) }) + const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res) + if (!indexesToInvalidate.length) return Promise.resolve() + return queries.batchInvalidate(indexesToInvalidate) + }) } -const errorAlertsNotify = (alertRec) => { +const errorAlertsNotify = alertRec => { const embedDeviceId = deviceId => _.assign({ deviceId }) - const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts)) + const mapToAlerts = _.map(it => + _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts), + ) const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices) return clearOldErrorNotifications(alerts).then(() => { _.forEach(alert => { switch (alert.code) { case PING: { - const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId }) - return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => { - if (res.length > 0) return Promise.resolve() - const message = `Machine down` - return queries.addNotification(ERROR, message, detailB) + const detailB = utils.buildDetail({ + code: PING, + age: alert.age ? alert.age : -1, + deviceId: alert.deviceId, }) + return queries + .getValidNotifications(ERROR, _.omit(['age'], detailB)) + .then(res => { + if (res.length > 0) return Promise.resolve() + const message = `Machine down` + return queries.addNotification(ERROR, message, detailB) + }) } case STALE: { - const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId }) + const detailB = utils.buildDetail({ + code: STALE, + deviceId: alert.deviceId, + }) return queries.getValidNotifications(ERROR, detailB).then(res => { if (res.length > 0) return Promise.resolve() const message = `Machine is stuck on ${alert.state} screen` @@ -182,18 +244,39 @@ const errorAlertsNotify = (alertRec) => { }) } -function notifCenterTransactionNotify (isHighValue, direction, fiat, fiatCode, deviceId, cryptoAddress) { +function notifCenterTransactionNotify( + isHighValue, + direction, + fiat, + fiatCode, + deviceId, + cryptoAddress, +) { const messageSuffix = isHighValue ? 'High value' : '' const message = `${messageSuffix} ${fiat} ${fiatCode} ${direction} transaction` - const detailB = utils.buildDetail({ deviceId: deviceId, direction, fiat, fiatCode, cryptoAddress }) - return queries.addNotification(isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX, message, detailB) + const detailB = utils.buildDetail({ + deviceId: deviceId, + direction, + fiat, + fiatCode, + cryptoAddress, + }) + return queries.addNotification( + isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX, + message, + detailB, + ) } const blacklistNotify = (tx, isAddressReuse) => { const code = isAddressReuse ? 'REUSED' : 'BLOCKED' const name = isAddressReuse ? 'reused' : 'blacklisted' - const detailB = utils.buildDetail({ cryptoCode: tx.cryptoCode, code, cryptoAddress: tx.toAddress }) + const detailB = utils.buildDetail({ + cryptoCode: tx.cryptoCode, + code, + cryptoAddress: tx.toAddress, + }) const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...` return queries.addNotification(COMPLIANCE, message, detailB) } @@ -211,5 +294,5 @@ module.exports = { errorAlertsNotify, notifCenterTransactionNotify, blacklistNotify, - cashboxNotify + cashboxNotify, } diff --git a/packages/server/lib/notifier/queries.js b/packages/server/lib/notifier/queries.js index 7c33aecd..c3fcd721 100644 --- a/packages/server/lib/notifier/queries.js +++ b/packages/server/lib/notifier/queries.js @@ -15,18 +15,22 @@ compliance - notifications related to warnings triggered by compliance settings error - notifications related to errors */ -function getMachineName (machineId) { +function getMachineName(machineId) { const sql = 'SELECT * FROM devices WHERE device_id=$1' - return db.oneOrNone(sql, [machineId]) - .then(it => it.name).catch(logger.error) + return db + .oneOrNone(sql, [machineId]) + .then(it => it.name) + .catch(logger.error) } const addNotification = (type, message, detail) => { const sql = `INSERT INTO notifications (id, type, message, detail) VALUES ($1, $2, $3, $4)` - return db.oneOrNone(sql, [uuidv4(), type, message, detail]).catch(logger.error) + return db + .oneOrNone(sql, [uuidv4(), type, message, detail]) + .catch(logger.error) } -const getAllValidNotifications = (type) => { +const getAllValidNotifications = type => { const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'` return db.any(sql, [type]).catch(logger.error) } @@ -37,7 +41,7 @@ const invalidateNotification = (detail, type) => { return db.none(sql, [type, detail]).catch(logger.error) } -const batchInvalidate = (ids) => { +const batchInvalidate = ids => { const formattedIds = _.map(pgp.as.text, ids).join(',') const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id IN ($1^)` return db.none(sql, [formattedIds]).catch(logger.error) @@ -79,7 +83,10 @@ const hasUnreadNotifications = () => { (SELECT * FROM notifications WHERE NOT read AND ${WITHIN_PAST_WEEK}) ` - return db.oneOrNone(sql).then(res => res.exists).catch(logger.error) + return db + .oneOrNone(sql) + .then(res => res.exists) + .catch(logger.error) } const getAlerts = () => { @@ -105,5 +112,5 @@ module.exports = { markAllAsRead, hasUnreadNotifications, getAlerts, - getMachineName + getMachineName, } diff --git a/packages/server/lib/notifier/sms.js b/packages/server/lib/notifier/sms.js index 65a64251..ce89da80 100644 --- a/packages/server/lib/notifier/sms.js +++ b/packages/server/lib/notifier/sms.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') const utils = require('./utils') const sms = require('../sms') -function printSmsAlerts (alertRec, config) { +function printSmsAlerts(alertRec, config) { let alerts = [] if (config.balance) { @@ -21,31 +21,33 @@ function printSmsAlerts (alertRec, config) { const code = entry[0] const machineNames = _.filter( _.negate(_.isEmpty), - _.map('machineName', entry[1]) + _.map('machineName', entry[1]), ) const cryptoCodes = _.filter( _.negate(_.isEmpty), - _.map('cryptoCode', entry[1]) + _.map('cryptoCode', entry[1]), ) return { codeDisplay: utils.codeDisplay(code), machineNames, - cryptoCodes + cryptoCodes, } }, _.toPairs(alertsMap)) const mapByCodeDisplay = _.map(it => { - if (_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) return it.codeDisplay - if (_.isEmpty(it.machineNames)) return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})` + if (_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) + return it.codeDisplay + if (_.isEmpty(it.machineNames)) + return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})` return `${it.codeDisplay} (${it.machineNames.join(', ')})` }) const displayAlertTypes = _.compose( _.uniq, mapByCodeDisplay, - _.sortBy('codeDisplay') + _.sortBy('codeDisplay'), )(alertTypes) return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ') diff --git a/packages/server/lib/notifier/test/email.test.js b/packages/server/lib/notifier/test/email.test.js index 8d27b789..f5eae921 100644 --- a/packages/server/lib/notifier/test/email.test.js +++ b/packages/server/lib/notifier/test/email.test.js @@ -5,14 +5,14 @@ const alertRec = { f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { balanceAlerts: [], deviceAlerts: [ - { code: 'PING', age: 602784301.446, machineName: 'Abc123' } - ] - } + { code: 'PING', age: 602784301.446, machineName: 'Abc123' }, + ], + }, }, deviceNames: { - f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123', }, - general: [] + general: [], } const printEmailMsg = `Errors were reported by your Lamassu Machines. @@ -23,6 +23,6 @@ Machine down for ~6 days test('Print Email Alers', () => { expect(email.printEmailAlerts(alertRec, { active: true, errors: true })).toBe( - printEmailMsg + printEmailMsg, ) }) diff --git a/packages/server/lib/notifier/test/notifier.test.js b/packages/server/lib/notifier/test/notifier.test.js index f49c1098..211c9d0c 100644 --- a/packages/server/lib/notifier/test/notifier.test.js +++ b/packages/server/lib/notifier/test/notifier.test.js @@ -20,7 +20,7 @@ const plugins = { email_errors: false, sms_errors: true, sms: { active: true, errors: true }, - email: { active: false, errors: false } + email: { active: false, errors: false }, }), getMachineNames: () => [ { @@ -36,16 +36,15 @@ const plugins = { name: 'Abc123', paired: true, cashOut: true, - statuses: [{ label: 'Unresponsive', type: 'error' }] - } + statuses: [{ label: 'Unresponsive', type: 'error' }], + }, ], - checkBalances: () => [] + checkBalances: () => [], } const tx = { id: 'bec8d452-9ea2-4846-841b-55a9df8bbd00', - deviceId: - '490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88', + deviceId: '490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88', toAddress: 'bc1q7s4yy5n9vp6zhlf6mrw3cttdgx5l3ysr2mhc4v', cryptoAtoms: new BN(252100), cryptoCode: 'BTC', @@ -71,7 +70,7 @@ const tx = { commissionPercentage: new BN(0.11), rawTickerPrice: new BN(18937.4), isPaperWallet: false, - direction: 'cashIn' + direction: 'cashIn', } const notifSettings = { @@ -84,13 +83,13 @@ const notifSettings = { sms: { active: true, errors: true, - transactions: false // force early return + transactions: false, // force early return }, email: { active: false, errors: false, - transactions: false // force early return - } + transactions: false, // force early return + }, } test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => { @@ -99,9 +98,9 @@ test('Exits checkNotifications with Promise.resolve() if SMS and Email are disab notifier.checkNotification({ getNotificationConfig: () => ({ sms: { active: false, errors: false }, - email: { active: false, errors: false } - }) - }) + email: { active: false, errors: false }, + }), + }), ).resolves.toBe(undefined) }) @@ -111,9 +110,9 @@ test('Exits checkNotifications with Promise.resolve() if SMS and Email are disab notifier.checkNotification({ getNotificationConfig: () => ({ sms: { active: false, errors: true, balance: true }, - email: { active: false, errors: true, balance: true } - }) - }) + email: { active: false, errors: true, balance: true }, + }), + }), ).resolves.toBe(undefined) }) @@ -124,13 +123,13 @@ test("Check Pings should return code PING for devices that haven't been pinged r deviceId: '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', lastPing: '2020-11-16T13:11:03.169Z', - name: 'Abc123' - } - ]) + name: 'Abc123', + }, + ]), ).toMatchObject({ '7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [ - { code: 'PING', machineName: 'Abc123' } - ] + { code: 'PING', machineName: 'Abc123' }, + ], }) }) @@ -141,11 +140,11 @@ test('Checkpings returns empty array as the value for the id prop, if the lastPi deviceId: '7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4', lastPing: new Date(), - name: 'Abc123' - } - ]) + name: 'Abc123', + }, + ]), ).toMatchObject({ - '7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [] + '7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [], }) }) @@ -203,7 +202,7 @@ test('checkStuckScreen returns [] if most recent event is idle', () => { note: '{"state":"chooseCoin","isIdle":false}', created: '2020-11-23T19:30:29.209Z', device_time: '1999-11-23T19:30:29.177Z', - age: 157352628.123 + age: 157352628.123, }, { id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', @@ -213,9 +212,9 @@ test('checkStuckScreen returns [] if most recent event is idle', () => { note: '{"state":"chooseCoin","isIdle":true}', created: '2020-11-23T19:30:29.209Z', device_time: '2020-11-23T19:30:29.177Z', - age: 157352628.123 - } - ]) + age: 157352628.123, + }, + ]), ).toEqual([]) }) @@ -230,7 +229,7 @@ test('checkStuckScreen returns object array of length 1 with prop code: "STALE" note: '{"state":"chooseCoin","isIdle":true}', created: '2020-11-23T19:30:29.209Z', device_time: '1999-11-23T19:30:29.177Z', - age: 0 + age: 0, }, { id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2', @@ -240,8 +239,8 @@ test('checkStuckScreen returns object array of length 1 with prop code: "STALE" note: '{"state":"chooseCoin","isIdle":false}', created: '2020-11-23T19:30:29.209Z', device_time: '2020-11-23T19:30:29.177Z', - age: 157352628.123 - } + age: 157352628.123, + }, ]) expect(result[0]).toMatchObject({ code: 'STALE' }) }) @@ -257,8 +256,8 @@ test('checkStuckScreen returns empty array if age < STALE_STATE', () => { note: '{"state":"chooseCoin","isIdle":false}', created: '2020-11-23T19:30:29.209Z', device_time: '2020-11-23T19:30:29.177Z', - age: 0 - } + age: 0, + }, ]) const result2 = notifier.checkStuckScreen([ { @@ -269,8 +268,8 @@ test('checkStuckScreen returns empty array if age < STALE_STATE', () => { note: '{"state":"chooseCoin","isIdle":false}', created: '2020-11-23T19:30:29.209Z', device_time: '2020-11-23T19:30:29.177Z', - age: STALE_STATE - } + age: STALE_STATE, + }, ]) expect(result1).toEqual([]) expect(result2).toEqual([]) @@ -281,7 +280,10 @@ test('calls sendRedemptionMessage if !zeroConf and rec.isRedemption', async () = const settingsLoader = require('../../new-settings-loader') const loadLatest = jest.spyOn(settingsLoader, 'loadLatest') - const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications') + const getGlobalNotifications = jest.spyOn( + configManager, + 'getGlobalNotifications', + ) const getWalletSettings = jest.spyOn(configManager, 'getWalletSettings') // sendRedemptionMessage will cause this func to be called @@ -289,19 +291,24 @@ test('calls sendRedemptionMessage if !zeroConf and rec.isRedemption', async () = getWalletSettings.mockReturnValue({ zeroConfLimit: -Infinity }) loadLatest.mockReturnValue(Promise.resolve({})) - getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true }, notificationCenter: { active: true } }) + getGlobalNotifications.mockReturnValue({ + ...notifSettings, + sms: { active: true, errors: true, transactions: true }, + notificationCenter: { active: true }, + }) const response = await notifier.transactionNotify(tx, { isRedemption: true }) // this type of response implies sendRedemptionMessage was called expect(response[0]).toMatchObject({ sms: { - body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully" + body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully", }, email: { - subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00", - body: 'It was just dispensed successfully' - } + subject: + "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00", + body: 'It was just dispensed successfully', + }, }) }) @@ -310,23 +317,32 @@ test('calls sendTransactionMessage if !zeroConf and !rec.isRedemption', async () const settingsLoader = require('../../new-settings-loader') const queries = require('../queries') const loadLatest = jest.spyOn(settingsLoader, 'loadLatest') - const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications') + const getGlobalNotifications = jest.spyOn( + configManager, + 'getGlobalNotifications', + ) const getWalletSettings = jest.spyOn(configManager, 'getWalletSettings') const getMachineName = jest.spyOn(queries, 'getMachineName') const buildTransactionMessage = jest.spyOn(utils, 'buildTransactionMessage') // sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue - jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({ prop: rec })) + jest + .spyOn(smsFuncs, 'sendMessage') + .mockImplementation((_, rec) => ({ prop: rec })) buildTransactionMessage.mockImplementation(() => ['mock message', false]) getMachineName.mockResolvedValue('mockMachineName') getWalletSettings.mockReturnValue({ zeroConfLimit: -Infinity }) loadLatest.mockReturnValue(Promise.resolve({})) - getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true }, notificationCenter: { active: true } }) + getGlobalNotifications.mockReturnValue({ + ...notifSettings, + sms: { active: true, errors: true, transactions: true }, + notificationCenter: { active: true }, + }) const response = await notifier.transactionNotify(tx, { isRedemption: false }) // If the return object is this, it means the code went through all the functions expected to go through if // getMachineName, buildTransactionMessage and sendTransactionMessage were called, in this order - expect(response).toEqual([{prop: 'mock message'}]) + expect(response).toEqual([{ prop: 'mock message' }]) }) diff --git a/packages/server/lib/notifier/test/sms.test.js b/packages/server/lib/notifier/test/sms.test.js index d42ac7ac..f4124c41 100644 --- a/packages/server/lib/notifier/test/sms.test.js +++ b/packages/server/lib/notifier/test/sms.test.js @@ -5,18 +5,18 @@ const alertRec = { f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { balanceAlerts: [], deviceAlerts: [ - { code: 'PING', age: 602784301.446, machineName: 'Abc123' } - ] - } + { code: 'PING', age: 602784301.446, machineName: 'Abc123' }, + ], + }, }, deviceNames: { - f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123', }, - general: [] + general: [], } test('Print SMS alerts', () => { expect(sms.printSmsAlerts(alertRec, { active: true, errors: true })).toBe( - '[Lamassu] Errors reported: Machine Down (Abc123)' + '[Lamassu] Errors reported: Machine Down (Abc123)', ) }) diff --git a/packages/server/lib/notifier/test/utils.test.js b/packages/server/lib/notifier/test/utils.test.js index aee67776..d6371455 100644 --- a/packages/server/lib/notifier/test/utils.test.js +++ b/packages/server/lib/notifier/test/utils.test.js @@ -3,7 +3,7 @@ const utils = require('../utils') const plugins = { sendMessage: rec => { return rec - } + }, } const alertRec = { @@ -11,19 +11,19 @@ const alertRec = { f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: { balanceAlerts: [], deviceAlerts: [ - { code: 'PING', age: 1605532263169, machineName: 'Abc123' } - ] - } + { code: 'PING', age: 1605532263169, machineName: 'Abc123' }, + ], + }, }, deviceNames: { - f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123' + f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123', }, - general: [] + general: [], } const notifications = { sms: { active: true, errors: true }, - email: { active: false, errors: false } + email: { active: false, errors: false }, } describe('buildAlertFingerprint', () => { @@ -33,37 +33,37 @@ describe('buildAlertFingerprint', () => { { devices: {}, deviceNames: {}, - general: [] + general: [], }, - notifications - ) + notifications, + ), ).toBe(null) }) - + test('Build alert fingerprint returns null if sms and email are disabled', () => { expect( utils.buildAlertFingerprint(alertRec, { sms: { active: false, errors: true }, - email: { active: false, errors: false } - }) + email: { active: false, errors: false }, + }), ).toBe(null) }) - + test('Build alert fingerprint returns hash if email or [sms] are enabled and there are alerts in alertrec', () => { expect( typeof utils.buildAlertFingerprint(alertRec, { sms: { active: true, errors: true }, - email: { active: false, errors: false } - }) + email: { active: false, errors: false }, + }), ).toBe('string') }) - + test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => { expect( typeof utils.buildAlertFingerprint(alertRec, { sms: { active: false, errors: false }, - email: { active: true, errors: true } - }) + email: { active: true, errors: true }, + }), ).toBe('string') }) }) @@ -76,8 +76,8 @@ describe('sendNoAlerts', () => { test('Send no alerts returns object with sms prop with sms only enabled', () => { expect(utils.sendNoAlerts(plugins, true, false)).toEqual({ sms: { - body: '[Lamassu] All clear' - } + body: '[Lamassu] All clear', + }, }) }) @@ -85,11 +85,11 @@ describe('sendNoAlerts', () => { expect(utils.sendNoAlerts(plugins, true, true)).toEqual({ email: { body: 'No errors are reported for your machines.', - subject: '[Lamassu] All clear' + subject: '[Lamassu] All clear', }, sms: { - body: '[Lamassu] All clear' - } + body: '[Lamassu] All clear', + }, }) }) @@ -97,8 +97,8 @@ describe('sendNoAlerts', () => { expect(utils.sendNoAlerts(plugins, false, true)).toEqual({ email: { body: 'No errors are reported for your machines.', - subject: '[Lamassu] All clear' - } + subject: '[Lamassu] All clear', + }, }) }) }) diff --git a/packages/server/lib/notifier/utils.js b/packages/server/lib/notifier/utils.js index a7880667..f02c0e8a 100644 --- a/packages/server/lib/notifier/utils.js +++ b/packages/server/lib/notifier/utils.js @@ -7,7 +7,7 @@ const { CODES_DISPLAY, NETWORK_DOWN_TIME, PING, - ALERT_SEND_INTERVAL + ALERT_SEND_INTERVAL, } = require('./codes') const DETAIL_TEMPLATE = { @@ -20,16 +20,17 @@ const DETAIL_TEMPLATE = { cryptoAddress: '', direction: '', fiat: '', - fiatCode: '' + fiatCode: '', } -function parseEventNote (event) { +function parseEventNote(event) { return _.set('note', JSON.parse(event.note), event) } -function checkPing (device) { - const age = Date.now() - (new Date(device.lastPing).getTime()) - if (age > NETWORK_DOWN_TIME) return [{ code: PING, age, machineName: device.name }] +function checkPing(device) { + const age = Date.now() - new Date(device.lastPing).getTime() + if (age > NETWORK_DOWN_TIME) + return [{ code: PING, age, machineName: device.name }] return [] } @@ -41,7 +42,7 @@ const codeDisplay = code => CODES_DISPLAY[code] const alertFingerprint = { fingerprint: null, - lastAlertTime: null + lastAlertTime: null, } const getAlertFingerprint = () => alertFingerprint.fingerprint @@ -60,7 +61,7 @@ const shouldNotAlert = currentAlertFingerprint => { ) } -function buildAlertFingerprint (alertRec, notifications) { +function buildAlertFingerprint(alertRec, notifications) { const sms = getAlertTypes(alertRec, notifications.sms) const email = getAlertTypes(alertRec, notifications.email) if (sms.length === 0 && email.length === 0) return null @@ -72,7 +73,7 @@ function buildAlertFingerprint (alertRec, notifications) { return crypto.createHash('sha256').update(subject).digest('hex') } -function sendNoAlerts (plugins, smsEnabled, emailEnabled) { +function sendNoAlerts(plugins, smsEnabled, emailEnabled) { const subject = '[Lamassu] All clear' let rec = {} @@ -83,14 +84,20 @@ function sendNoAlerts (plugins, smsEnabled, emailEnabled) { if (emailEnabled) { rec = _.set(['email', 'subject'])(subject)(rec) rec = _.set(['email', 'body'])('No errors are reported for your machines.')( - rec + rec, ) } return plugins.sendMessage(rec) } -const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) => { +const buildTransactionMessage = ( + tx, + rec, + highValueTx, + machineName, + customer, +) => { const isCashOut = tx.direction === 'cashOut' const direction = isCashOut ? 'Cash Out' : 'Cash In' const crypto = `${coinUtils.toUnit(tx.cryptoAtoms, tx.cryptoCode)} ${ @@ -124,41 +131,49 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) => const smsSubject = `A ${highValueTx ? 'high value ' : ''}${direction.toLowerCase()} transaction just happened at ${machineName} for ${fiat}` const emailSubject = `A ${highValueTx ? 'high value ' : ''}transaction just happened` - return [{ - sms: { - body: `${smsSubject} – ${status}` + return [ + { + sms: { + body: `${smsSubject} – ${status}`, + }, + email: { + emailSubject, + body, + }, + webhook: { + topic: `New transaction`, + content: body, + }, }, - email: { - emailSubject, - body - }, - webhook: { - topic: `New transaction`, - content: body - } - }, highValueTx] + highValueTx, + ] } -function formatCurrency (num = 0, code) { - const formattedNumber = Number(num).toLocaleString(undefined, {maximumFractionDigits:2, minimumFractionDigits:2}) +function formatCurrency(num = 0, code) { + const formattedNumber = Number(num).toLocaleString(undefined, { + maximumFractionDigits: 2, + minimumFractionDigits: 2, + }) return `${formattedNumber} ${code}` } -function formatAge (age, settings) { +function formatAge(age, settings) { return prettyMs(age, settings) } -function buildDetail (obj) { +function buildDetail(obj) { // obj validation const objKeys = _.keys(obj) const detailKeys = _.keys(DETAIL_TEMPLATE) - if ((_.difference(objKeys, detailKeys)).length > 0) { - return Promise.reject(new Error('Error when building detail object: invalid properties')) + if (_.difference(objKeys, detailKeys).length > 0) { + return Promise.reject( + new Error('Error when building detail object: invalid properties'), + ) } return { ...DETAIL_TEMPLATE, ...obj } } -function deviceAlerts (config, alertRec, device) { +function deviceAlerts(config, alertRec, device) { let alerts = [] if (config.balance) { alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) @@ -170,7 +185,7 @@ function deviceAlerts (config, alertRec, device) { return alerts } -function getAlertTypes (alertRec, config) { +function getAlertTypes(alertRec, config) { let alerts = [] if (!isActive(config)) return alerts @@ -200,5 +215,5 @@ module.exports = { formatCurrency, formatAge, buildDetail, - deviceAlerts + deviceAlerts, } diff --git a/packages/server/lib/notifier/webhook.js b/packages/server/lib/notifier/webhook.js index fcddf1ed..015d151e 100644 --- a/packages/server/lib/notifier/webhook.js +++ b/packages/server/lib/notifier/webhook.js @@ -12,10 +12,10 @@ const sendMessage = (settings, rec) => { return axios({ method: 'POST', url: WEBHOOK_URL, - data: body + data: body, }) } module.exports = { - sendMessage + sendMessage, } diff --git a/packages/server/lib/ofac/index.js b/packages/server/lib/ofac/index.js index 8f34027b..3847d2d4 100644 --- a/packages/server/lib/ofac/index.js +++ b/packages/server/lib/ofac/index.js @@ -10,7 +10,7 @@ const OFAC_DATA_DIR = process.env.OFAC_DATA_DIR let structs = null -function load () { +function load() { if (!OFAC_DATA_DIR) { const message = 'The ofacDataDir option has not been set in the environment' return Promise.reject(new Error(message)) @@ -19,10 +19,12 @@ function load () { const ofacSourcesDir = path.join(OFAC_DATA_DIR, 'sources') return readdir(ofacSourcesDir) - .then(_.flow( - _.map(file => path.join(ofacSourcesDir, file)), - loader.load - )) + .then( + _.flow( + _.map(file => path.join(ofacSourcesDir, file)), + loader.load, + ), + ) .then(result => { return (structs = result) }) @@ -30,14 +32,14 @@ function load () { // nameParts should be an object like {firstName: "John", lastName: "Doe", ...} -function makeCompatible (nameParts) { +function makeCompatible(nameParts) { const partNames = _.keys(nameParts) const values = _.map(_.lowerCase, _.values(nameParts)) const props = _.zipAll([partNames, values]) return _.map(_.zipObject(['partName', 'value']), props) } -function match (nameParts, birthDateString, options) { +function match(nameParts, birthDateString, options) { if (!structs) { logger.error(new Error('The OFAC data sources have not been loaded yet.')) return false @@ -50,24 +52,27 @@ function match (nameParts, birthDateString, options) { // birthDateString is in YYYYMMDD format const birthDate = _.cond([ - [_.identity, () => { - const year = parseInt(birthDateString.slice(0, 4)) - const month = parseInt(birthDateString.slice(4, 6)) - const day = parseInt(birthDateString.slice(6, 8)) - const date = new Date(year, month - 1, day) + [ + _.identity, + () => { + const year = parseInt(birthDateString.slice(0, 4)) + const month = parseInt(birthDateString.slice(4, 6)) + const day = parseInt(birthDateString.slice(6, 8)) + const date = new Date(year, month - 1, day) - return {year, month, day, date} - }], - [_.stubTrue, () => null] + return { year, month, day, date } + }, + ], + [_.stubTrue, () => null], ])(birthDateString) - const candidate = {parts, fullName, words, birthDate} + const candidate = { parts, fullName, words, birthDate } const result = matcher.match(structs, candidate, options) return result } -function getStructs () { +function getStructs() { return structs } -module.exports = {load, match, getStructs} +module.exports = { load, match, getStructs } diff --git a/packages/server/lib/ofac/loading.js b/packages/server/lib/ofac/loading.js index 8c422977..0b1c59c4 100644 --- a/packages/server/lib/ofac/loading.js +++ b/packages/server/lib/ofac/loading.js @@ -2,20 +2,18 @@ const fs = require('fs') const ndjson = require('ndjson') const _ = require('lodash/fp') - const mapAliases = _.curry((iteratee, individuals) => { const mapIndividual = individual => { - const {id, aliases} = individual + const { id, aliases } = individual return _.map(alias => iteratee(id, alias), aliases) } return _.flatMap(mapIndividual, individuals) }) - const getPhoneticEntries = (individualId, alias) => { const pairPhoneticsWithValues = word => { - const {value, phonetics} = word - const makeEntry = phonetic => ({value, phonetic, aliasId: alias.id}) + const { value, phonetics } = word + const makeEntry = phonetic => ({ value, phonetic, aliasId: alias.id }) return _.map(makeEntry, phonetics) } return _.flatMap(pairPhoneticsWithValues, alias.words) @@ -25,17 +23,13 @@ const producePhoneticMap = _.flow( mapAliases(getPhoneticEntries), _.flatten, _.groupBy(_.get('phonetic')), - _.mapValues(_.flow( - _.map(_.get('aliasId')), - _.uniq - )), + _.mapValues(_.flow(_.map(_.get('aliasId')), _.uniq)), _.toPairs, - entries => new Map(entries) + entries => new Map(entries), ) - const getWords = (individualId, alias) => { - const pairWordsWithIds = word => ({value: word.value, aliasId: alias.id}) + const pairWordsWithIds = word => ({ value: word.value, aliasId: alias.id }) return _.map(pairWordsWithIds, alias.words) } @@ -45,7 +39,7 @@ const produceWordList = _.flow( _.groupBy(_.get('value')), _.mapValues(_.map(_.get('aliasId'))), _.toPairs, - _.map(_.zipObject(['value', 'aliasIds'])) + _.map(_.zipObject(['value', 'aliasIds'])), ) const parseSource = source => { @@ -55,10 +49,13 @@ const parseSource = source => { const jsonStream = readStream.pipe(ndjson.parse()) jsonStream.on('data', individual => { _.forEach(period => { - _.forEach(date => { - const {year, month, day} = date - date.date = new Date(year, month - 1, day) - }, [period.start, period.end]) + _.forEach( + date => { + const { year, month, day } = date + date.date = new Date(year, month - 1, day) + }, + [period.start, period.end], + ) }, individual.birthDatePeriods) individuals.push(individual) }) @@ -71,18 +68,14 @@ const parseSource = source => { }) } -const load = sources => Promise.all(_.map(parseSource, sources)) - .then(_.flow( - _.flatten, - _.compact, - _.uniqBy(_.get('id')), - individuals => { - +const load = sources => + Promise.all(_.map(parseSource, sources)).then( + _.flow(_.flatten, _.compact, _.uniqBy(_.get('id')), individuals => { const individualsMap = _.flow( _.groupBy(_.get('id')), _.mapValues(_.first), _.toPairs, - entries => new Map(entries) + entries => new Map(entries), )(individuals) const makeEntries = (individualId, alias) => [alias.id, alias] @@ -101,9 +94,9 @@ const load = sources => Promise.all(_.map(parseSource, sources)) aliasesMap, aliasToIndividual, phoneticMap, - wordList + wordList, } - } - )) + }), + ) -module.exports = {load} +module.exports = { load } diff --git a/packages/server/lib/ofac/matching.js b/packages/server/lib/ofac/matching.js index f8c089e8..460a333a 100644 --- a/packages/server/lib/ofac/matching.js +++ b/packages/server/lib/ofac/matching.js @@ -7,7 +7,7 @@ const stringSimilarity = _.curry(jaro) // birth date -function isDateWithinSomeDaysOfPeriod (period, date, days) { +function isDateWithinSomeDaysOfPeriod(period, date, days) { const inMillisecs = 24 * 60 * 60 * 1000 const startTime = period.start.date.getTime() - days * inMillisecs @@ -16,32 +16,35 @@ function isDateWithinSomeDaysOfPeriod (period, date, days) { const endTime = period.end.date.getTime() + days * inMillisecs const endDate = new Date(endTime) - return (startDate < date && date < endDate) + return startDate < date && date < endDate } const isBornTooLongSince = _.curry((days, dateObject, individual) => { if (!dateObject) return false if (_.isEmpty(individual.birthDatePeriods)) return false - const isWithinSomeYears = _.partialRight(isDateWithinSomeDaysOfPeriod, [dateObject.date, days]) + const isWithinSomeYears = _.partialRight(isDateWithinSomeDaysOfPeriod, [ + dateObject.date, + days, + ]) return !_.some(isWithinSomeYears, individual.birthDatePeriods) }) // algorithm -function match (structs, candidate, options) { - const {threshold, fullNameThreshold, ratio = 0.5, verboseFor} = options - const {fullName, words, birthDate} = candidate +function match(structs, candidate, options) { + const { threshold, fullNameThreshold, ratio = 0.5, verboseFor } = options + const { fullName, words, birthDate } = candidate // Accept aliases who's full name matches. const doesNameMatch = _.flow( _.get('fullName'), stringSimilarity(fullName), - _.lte(fullNameThreshold) + _.lte(fullNameThreshold), ) const aliases = _.flatMap(_.get('aliases'), structs.individuals) const aliasIdsFromFullName = _.flow( _.filter(doesNameMatch), - _.map(_.get('id')) + _.map(_.get('id')), )(aliases) const phoneticWeight = ratio @@ -60,12 +63,25 @@ function match (structs, candidate, options) { for (const aliasId of wordEntry.aliasIds) { const phoneticScore = phoneticMatches.has(aliasId) ? 1 : -1 - const finalScore = stringWeight * stringScore + phoneticWeight * phoneticScore + const finalScore = + stringWeight * stringScore + phoneticWeight * phoneticScore - verbose && logger.debug(finalScore.toFixed(2), stringScore.toFixed(2), phoneticScore.toFixed(2), word.value, wordEntry.value) + verbose && + logger.debug( + finalScore.toFixed(2), + stringScore.toFixed(2), + phoneticScore.toFixed(2), + word.value, + wordEntry.value, + ) if (finalScore >= threshold) { - const entry = {aliasId, score: finalScore, word: word.value, value: wordEntry.value} + const entry = { + aliasId, + score: finalScore, + word: word.value, + value: wordEntry.value, + } const index = _.sortedIndexBy(x => -x.score, entry, matches) matches.splice(index, 0, entry) } @@ -83,10 +99,10 @@ function match (structs, candidate, options) { _.countBy(_.identity), _.toPairs, _.filter(([aliasId, count]) => { - const {length} = structs.aliasesMap.get(aliasId).words - return (count >= _.min([2, words.length, length])) + const { length } = structs.aliasesMap.get(aliasId).words + return count >= _.min([2, words.length, length]) }), - _.map(_.first) + _.map(_.first), )(matches) // Get the full record for each matched id @@ -94,10 +110,9 @@ function match (structs, candidate, options) { const individualId = structs.aliasToIndividual.get(aliasId) return structs.individualsMap.get(individualId) } - const suspects = _.uniq(_.map(getIndividual, [ - ...aliasIdsFromFullName, - ...aliasIdsFromNamePart - ])) + const suspects = _.uniq( + _.map(getIndividual, [...aliasIdsFromFullName, ...aliasIdsFromNamePart]), + ) // Reject everyone who is born two years away. const twoYears = 365 * 2 @@ -105,4 +120,4 @@ function match (structs, candidate, options) { return _.reject(unqualified, suspects) } -module.exports = {match} +module.exports = { match } diff --git a/packages/server/lib/ofac/name-utils.js b/packages/server/lib/ofac/name-utils.js index 947befa9..de185267 100644 --- a/packages/server/lib/ofac/name-utils.js +++ b/packages/server/lib/ofac/name-utils.js @@ -5,17 +5,24 @@ const makePhonetic = _.flow(doubleMetaphone, _.uniq) // Combine name-parts in a standard order. -const partOrdering = ['firstName', 'middleName', 'maidenName', 'patronymic', 'matronymic', 'lastName'] +const partOrdering = [ + 'firstName', + 'middleName', + 'maidenName', + 'patronymic', + 'matronymic', + 'lastName', +] const usingPartOrder = _.flow( _.get('partName'), - _.partialRight(_.indexOf, [partOrdering]) + _.partialRight(_.indexOf, [partOrdering]), ) const makeFullName = _.flow( _.sortBy(usingPartOrder), _.map(_.get('value')), - _.join(' ') + _.join(' '), ) const makeWords = value => { @@ -27,5 +34,5 @@ const makeWords = value => { module.exports = { makeFullName, - makeWords + makeWords, } diff --git a/packages/server/lib/ofac/parsing.js b/packages/server/lib/ofac/parsing.js index 36b7dcd8..0c0b42d6 100644 --- a/packages/server/lib/ofac/parsing.js +++ b/packages/server/lib/ofac/parsing.js @@ -25,7 +25,7 @@ const partNames = new Map([ [MAIDEN_NAME, 'maidenName'], [PATRONYMIC, 'patronymic'], [MATRONYMIC, 'matronymic'], - [NICKNAME, 'nickname'] + [NICKNAME, 'nickname'], ]) const filteredWords = [ @@ -34,7 +34,7 @@ const filteredWords = [ // group-id to type-id -function processMasterNamePartGroup (groupNode) { +function processMasterNamePartGroup(groupNode) { const namePartGroupNode = groupNode.NamePartGroup const groupId = namePartGroupNode.$.ID const typeId = namePartGroupNode.$.NamePartTypeID @@ -47,7 +47,7 @@ const processDocumentedNamePart = _.curry((groupTypes, namePartNode) => { const typeId = groupTypes.get(groupId) const partName = partNames.get(typeId) const value = _.lowerCase(valueNode.$text) - return {partName, value} + return { partName, value } }) const isLatin = _.matchesProperty(['$', 'DocNameStatusID'], PRIMARY_LATIN) @@ -72,29 +72,26 @@ const processAlias = _.curry((groupTypes, aliasNode) => { const fullName = nameUtils.makeFullName(parts) const words = _.flow( nameUtils.makeWords, - _.reject(_.flow( - _.get('value'), - word => filteredWords.includes(word) - )) + _.reject(_.flow(_.get('value'), word => filteredWords.includes(word))), )(fullName) // if (words.length < 2) { // console.log(JSON.stringify(words)) // } - return {id, parts, fullName, words} + return { id, parts, fullName, words } }) // birth date -function processDate (dateNode) { +function processDate(dateNode) { const year = parseInt(dateNode.Year) const month = parseInt(dateNode.Month) const day = parseInt(dateNode.Day) - return {year, month, day} + return { year, month, day } } -function processFeature (featureNode) { +function processFeature(featureNode) { if (featureNode.$.FeatureTypeID !== BIRTH_DATE) return const datePeriodNode = featureNode.FeatureVersion.DatePeriod @@ -103,7 +100,7 @@ function processFeature (featureNode) { // By using Start.From and End.To we use the extremes of the date-period. const period = { start: datePeriodNode.Start.From, - end: datePeriodNode.End.To + end: datePeriodNode.End.To, } return _.mapValues(processDate, period) @@ -111,13 +108,16 @@ function processFeature (featureNode) { // profile -function processProfile (profileNode) { +function processProfile(profileNode) { if (profileNode.$.PartySubTypeID !== INDIVIDUAL) return const id = profileNode.$.ID const identityNode = profileNode.Identity - const groupTypesEntries = _.map(processMasterNamePartGroup, identityNode.NamePartGroups.MasterNamePartGroup) + const groupTypesEntries = _.map( + processMasterNamePartGroup, + identityNode.NamePartGroups.MasterNamePartGroup, + ) const groupTypes = new Map(groupTypesEntries) const mapCompact = _.flow(_.map, _.compact) @@ -128,7 +128,7 @@ function processProfile (profileNode) { if (_.isEmpty(aliases)) return const birthDatePeriods = mapCompact(processFeature, profileNode.Feature) - const individual = {id, aliases, birthDatePeriods} + const individual = { id, aliases, birthDatePeriods } return individual } @@ -158,4 +158,4 @@ const parse = (source, callback) => { }) } -module.exports = {parse} +module.exports = { parse } diff --git a/packages/server/lib/ofac/update.js b/packages/server/lib/ofac/update.js index 8c520a69..eda039ed 100644 --- a/packages/server/lib/ofac/update.js +++ b/packages/server/lib/ofac/update.js @@ -1,7 +1,14 @@ const parser = require('./parsing') const axios = require('axios') const { createWriteStream } = require('fs') -const { rename, writeFile, readFile, mkdir, copyFile, unlink } = require('fs/promises') +const { + rename, + writeFile, + readFile, + mkdir, + copyFile, + unlink, +} = require('fs/promises') const path = require('path') const _ = require('lodash/fp') @@ -10,17 +17,21 @@ const OFAC_DATA_DIR = process.env.OFAC_DATA_DIR const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources') const LAST_UPDATED_FILE = path.resolve(OFAC_DATA_DIR, 'last_updated.dat') -const OFAC_SOURCES = [{ - name: 'sdn_advanced', - url: 'https://sanctionslistservice.ofac.treas.gov/api/download/sdn_advanced.xml' -}, { - name: 'cons_advanced', - url: 'https://sanctionslistservice.ofac.treas.gov/api/download/cons_advanced.xml' -}] +const OFAC_SOURCES = [ + { + name: 'sdn_advanced', + url: 'https://sanctionslistservice.ofac.treas.gov/api/download/sdn_advanced.xml', + }, + { + name: 'cons_advanced', + url: 'https://sanctionslistservice.ofac.treas.gov/api/download/cons_advanced.xml', + }, +] const _mkdir = path => - mkdir(path) - .catch(err => err.code === 'EEXIST' ? Promise.resolve() : Promise.reject(err)) + mkdir(path).catch(err => + err.code === 'EEXIST' ? Promise.resolve() : Promise.reject(err), + ) const download = (dstDir, { name, url }) => { const dstFile = path.join(dstDir, name + '.xml') @@ -90,7 +101,7 @@ const moveToSourcesDir = async (srcFile, ofacSourcesDir) => { return dstFile } -function update () { +function update() { if (!OFAC_DATA_DIR) { throw new Error('ofacDataDir must be defined in the environment') } @@ -118,17 +129,20 @@ function update () { if (skipUpdate) return Promise.resolve() const downloads = _.flow( - _.map(file => download(DOWNLOAD_DIR, file).then(parseToJson)) + _.map(file => download(DOWNLOAD_DIR, file).then(parseToJson)), )(OFAC_SOURCES) - return Promise.all(downloads) - .then(parsed => { - const moves = _.map(src => moveToSourcesDir(src, OFAC_SOURCES_DIR), parsed) - const timestamp = new Date().toISOString() + return Promise.all(downloads).then(parsed => { + const moves = _.map( + src => moveToSourcesDir(src, OFAC_SOURCES_DIR), + parsed, + ) + const timestamp = new Date().toISOString() - return Promise.all([...moves]) - .then(() => writeFile(LAST_UPDATED_FILE, timestamp)) - }) + return Promise.all([...moves]).then(() => + writeFile(LAST_UPDATED_FILE, timestamp), + ) + }) }) } diff --git a/packages/server/lib/operator.js b/packages/server/lib/operator.js index ccd9ad5e..b6243f16 100644 --- a/packages/server/lib/operator.js +++ b/packages/server/lib/operator.js @@ -1,10 +1,9 @@ const db = require('./db') const _ = require('lodash/fp') -function getOperatorId (service) { +function getOperatorId(service) { const sql = `SELECT operator_id FROM operator_ids WHERE service = '${service}'` - return db.oneOrNone(sql) - .then(_.get('operator_id')) + return db.oneOrNone(sql).then(_.get('operator_id')) } module.exports = { getOperatorId } diff --git a/packages/server/lib/pairing.js b/packages/server/lib/pairing.js index 72e88f4c..38eecc9b 100644 --- a/packages/server/lib/pairing.js +++ b/packages/server/lib/pairing.js @@ -12,7 +12,7 @@ const CA_PATH = process.env.CA_PATH const DEFAULT_NUMBER_OF_CASSETTES = 2 const DEFAULT_NUMBER_OF_RECYCLERS = 0 -function pullToken (token) { +function pullToken(token) { const sql = `delete from pairing_tokens where token=$1 returning name, created < now() - interval '1 hour' as expired` @@ -20,24 +20,41 @@ function pullToken (token) { } // TODO new-admin: We should remove all configs related to that device. This can get tricky. -function unpair (deviceId) { - return db.tx(t => - t.none(`INSERT INTO unpaired_devices(id, device_id, name, model, paired, unpaired) +function unpair(deviceId) { + return db.tx(t => + t + .none( + `INSERT INTO unpaired_devices(id, device_id, name, model, paired, unpaired) SELECT $1, $2, d.name, d.model, d.created, now() FROM devices d - WHERE device_id=$2` - , [uuid.v4(), deviceId]) - .then(() => { + WHERE device_id=$2`, + [uuid.v4(), deviceId], + ) + .then(() => { const q1 = t.none(`DELETE FROM devices WHERE device_id=$1`, [deviceId]) - const q2 = t.none(`DELETE FROM machine_pings WHERE device_id=$1`, [deviceId]) - const q3 = t.none(`DELETE FROM machine_network_heartbeat WHERE device_id=$1`, [deviceId]) - const q4 = t.none(`DELETE FROM machine_network_performance WHERE device_id=$1`, [deviceId]) + const q2 = t.none(`DELETE FROM machine_pings WHERE device_id=$1`, [ + deviceId, + ]) + const q3 = t.none( + `DELETE FROM machine_network_heartbeat WHERE device_id=$1`, + [deviceId], + ) + const q4 = t.none( + `DELETE FROM machine_network_performance WHERE device_id=$1`, + [deviceId], + ) return t.batch([q1, q2, q3, q4]) - }) + }), ) } -function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES, numOfRecyclers = DEFAULT_NUMBER_OF_RECYCLERS) { +function pair( + token, + deviceId, + machineModel, + numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES, + numOfRecyclers = DEFAULT_NUMBER_OF_RECYCLERS, +) { return pullToken(token) .then(r => { if (r.expired) return false @@ -46,7 +63,8 @@ function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF on conflict (device_id) do update set paired=TRUE, display=TRUE` - return db.none(insertSql, [deviceId, r.name, numOfCassettes, numOfRecyclers]) + return db + .none(insertSql, [deviceId, r.name, numOfCassettes, numOfRecyclers]) .then(() => true) }) .catch(err => { @@ -55,20 +73,21 @@ function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF }) } -function authorizeCaDownload (caToken) { - return pullToken(caToken) - .then(r => { - if (r.expired) throw new Error('Expired') +function authorizeCaDownload(caToken) { + return pullToken(caToken).then(r => { + if (r.expired) throw new Error('Expired') - return readFile(CA_PATH, {encoding: 'utf8'}) - }) + return readFile(CA_PATH, { encoding: 'utf8' }) + }) } -function isPaired (deviceId) { - const sql = 'select device_id, name from devices where device_id=$1 and paired=TRUE' +function isPaired(deviceId) { + const sql = + 'select device_id, name from devices where device_id=$1 and paired=TRUE' - return db.oneOrNone(sql, [deviceId]) - .then(row => row && row.device_id === deviceId ? row.name : false) + return db + .oneOrNone(sql, [deviceId]) + .then(row => (row && row.device_id === deviceId ? row.name : false)) } -module.exports = {pair, unpair, authorizeCaDownload, isPaired} +module.exports = { pair, unpair, authorizeCaDownload, isPaired } diff --git a/packages/server/lib/pg-transport.js b/packages/server/lib/pg-transport.js index 3d757a46..4ce823de 100644 --- a/packages/server/lib/pg-transport.js +++ b/packages/server/lib/pg-transport.js @@ -6,7 +6,7 @@ const eventBus = require('./event-bus') // of the base functionality and `.exceptions.handle()`. // module.exports = class CustomTransport extends Transport { - constructor (opts) { + constructor(opts) { super(opts) // @@ -24,7 +24,7 @@ module.exports = class CustomTransport extends Transport { this.connectionString = opts.connectionString } - log (level, message, meta, callback) { + log(level, message, meta, callback) { if (!callback) callback = () => {} setImmediate(() => { @@ -37,7 +37,7 @@ module.exports = class CustomTransport extends Transport { callback() } - logException (msg, meta, callback) { + logException(msg, meta, callback) { this.log('error', msg, meta, callback) } } diff --git a/packages/server/lib/plugin-helper.js b/packages/server/lib/plugin-helper.js index de189979..04e43dfe 100644 --- a/packages/server/lib/plugin-helper.js +++ b/packages/server/lib/plugin-helper.js @@ -1,6 +1,3 @@ -const path = require('path') -const fs = require('fs') - const _ = require('lodash/fp') const pluginCodes = { @@ -12,12 +9,12 @@ const pluginCodes = { SMS: 'sms', EMAIL: 'email', ZERO_CONF: 'zero-conf', - COMPLIANCE: 'compliance' + COMPLIANCE: 'compliance', } -module.exports = _.assign({load}, pluginCodes) +module.exports = _.assign({ load }, pluginCodes) -function load (type, pluginCode) { +function load(type, pluginCode) { if (!_.includes(type, _.values(pluginCodes))) { throw new Error(`Unallowed plugin type: ${type}`) } diff --git a/packages/server/lib/plugins.js b/packages/server/lib/plugins.js index 477809a5..c180f740 100644 --- a/packages/server/lib/plugins.js +++ b/packages/server/lib/plugins.js @@ -37,7 +37,7 @@ const notifier = require('./notifier') const { utils: coinUtils } = require('@lamassu/coins') const mapValuesWithKey = _.mapValues.convert({ - cap: false + cap: false, }) const TRADE_TTL = 2 * T.minutes @@ -45,9 +45,8 @@ const STALE_TICKER = 3 * T.minutes const STALE_BALANCE = 3 * T.minutes const tradesQueues = {} -function plugins (settings, deviceId) { - - function internalBuildRates (tickers, withCommission = true) { +function plugins(settings, deviceId) { + function internalBuildRates(tickers, withCommission = true) { const localeConfig = configManager.getLocale(deviceId, settings.config) const cryptoCodes = localeConfig.cryptoCurrencies @@ -55,43 +54,54 @@ function plugins (settings, deviceId) { cryptoCodes.forEach((cryptoCode, i) => { const rateRec = tickers[i] - const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + const commissions = configManager.getCommissions( + cryptoCode, + deviceId, + settings.config, + ) if (!rateRec) return - const cashInCommission = new BN(1).plus(new BN(commissions.cashIn).div(100)) + const cashInCommission = new BN(1).plus( + new BN(commissions.cashIn).div(100), + ) const cashOutCommission = _.isNil(commissions.cashOut) ? undefined : new BN(1).plus(new BN(commissions.cashOut).div(100)) - if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) + if (Date.now() - rateRec.timestamp > STALE_TICKER) + return logger.warn('Stale rate for ' + cryptoCode) const rate = rateRec.rates - withCommission ? rates[cryptoCode] = { - cashIn: rate.ask.times(cashInCommission).decimalPlaces(5), - cashOut: cashOutCommission && rate.bid.div(cashOutCommission).decimalPlaces(5) - } : rates[cryptoCode] = { - cashIn: rate.ask.decimalPlaces(5), - cashOut: rate.bid.decimalPlaces(5) - } + withCommission + ? (rates[cryptoCode] = { + cashIn: rate.ask.times(cashInCommission).decimalPlaces(5), + cashOut: + cashOutCommission && + rate.bid.div(cashOutCommission).decimalPlaces(5), + }) + : (rates[cryptoCode] = { + cashIn: rate.ask.decimalPlaces(5), + cashOut: rate.bid.decimalPlaces(5), + }) }) return rates } - function buildRatesNoCommission (tickers) { + function buildRatesNoCommission(tickers) { return internalBuildRates(tickers, false) } - function buildRates (tickers) { + function buildRates(tickers) { return internalBuildRates(tickers, true) } - function getNotificationConfig () { + function getNotificationConfig() { return configManager.getGlobalNotifications(settings.config) } - function buildBalances (balanceRecs) { + function buildBalances(balanceRecs) { const localeConfig = configManager.getLocale(deviceId, settings.config) const cryptoCodes = localeConfig.cryptoCurrencies @@ -99,8 +109,10 @@ function plugins (settings, deviceId) { cryptoCodes.forEach((cryptoCode, i) => { const balanceRec = balanceRecs[i] - if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet') - if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode) + if (!balanceRec) + return logger.warn('No balance for ' + cryptoCode + ' yet') + if (Date.now() - balanceRec.timestamp > STALE_BALANCE) + return logger.warn('Stale balance for ' + cryptoCode) balances[cryptoCode] = balanceRec.balance }) @@ -108,8 +120,11 @@ function plugins (settings, deviceId) { return balances } - function isZeroConf (tx) { - const walletSettings = configManager.getWalletSettings(tx.cryptoCode, settings.config) + function isZeroConf(tx) { + const walletSettings = configManager.getWalletSettings( + tx.cryptoCode, + settings.config, + ) const zeroConfLimit = walletSettings.zeroConfLimit || 0 return tx.fiat.lte(zeroConfLimit) } @@ -119,141 +134,179 @@ function plugins (settings, deviceId) { // cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations const cashUnitsBills = _.flow( _.get(['bills']), - _.filter(it => _.includes(cashUnitType, it.name) && it.denomination > 0), + _.filter( + it => _.includes(cashUnitType, it.name) && it.denomination > 0, + ), _.zip(cashUnits), )(tx) - const sameDenominations = ([cashUnit, bill]) => cashUnit?.denomination === bill?.denomination + const sameDenominations = ([cashUnit, bill]) => + cashUnit?.denomination === bill?.denomination if (!_.every(sameDenominations, cashUnitsBills)) - throw new Error(`Denominations don't add up, ${cashUnitType}s were changed.`) + throw new Error( + `Denominations don't add up, ${cashUnitType}s were changed.`, + ) return _.map( - ([cashUnit, { provisioned }]) => _.set('count', cashUnit.count - provisioned, cashUnit), - cashUnitsBills + ([cashUnit, { provisioned }]) => + _.set('count', cashUnit.count - provisioned, cashUnit), + cashUnitsBills, ) } return _.reduce(kons, cashUnits, redeemableTxs) } - function computeAvailableCassettes (cassettes, redeemableTxs) { + function computeAvailableCassettes(cassettes, redeemableTxs) { if (_.isEmpty(redeemableTxs)) return cassettes cassettes = accountProvisioned('cassette', cassettes, redeemableTxs) if (_.some(({ count }) => count < 0, cassettes)) - throw new Error('Negative note count: %j', counts) + throw new Error( + `Negative note count detected: ${JSON.stringify(cassettes)}`, + ) return cassettes } - function computeAvailableRecyclers (recyclers, redeemableTxs) { + function computeAvailableRecyclers(recyclers, redeemableTxs) { if (_.isEmpty(redeemableTxs)) return recyclers recyclers = accountProvisioned('recycler', recyclers, redeemableTxs) if (_.some(({ count }) => count < 0, recyclers)) - throw new Error('Negative note count: %j', counts) + throw new Error( + `Negative note count detected: ${JSON.stringify(recyclers)}`, + ) return recyclers } - function buildAvailableCassettes (excludeTxId) { + function buildAvailableCassettes(excludeTxId) { const cashOutConfig = configManager.getCashOut(deviceId, settings.config) if (!cashOutConfig.active) return Promise.resolve() - return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)]) - .then(([{ counts, numberOfCassettes }, redeemableTxs]) => { - redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), redeemableTxs) + return Promise.all([ + dbm.cassetteCounts(deviceId), + cashOutHelper.redeemableTxs(deviceId, excludeTxId), + ]).then(([{ counts, numberOfCassettes }, redeemableTxs]) => { + redeemableTxs = _.reject( + _.matchesProperty('id', excludeTxId), + redeemableTxs, + ) - const denominations = _.map( - it => cashOutConfig[`cassette${it}`], - _.range(1, numberOfCassettes+1) - ) - - if (counts.length !== denominations.length) - throw new Error('Denominations and respective counts do not match!') + const denominations = _.map( + it => cashOutConfig[`cassette${it}`], + _.range(1, numberOfCassettes + 1), + ) - const cassettes = _.map( - it => ({ - name: `cassette${it + 1}`, - denomination: parseInt(denominations[it], 10), - count: parseInt(counts[it], 10) - }), - _.range(0, numberOfCassettes) - ) + if (counts.length !== denominations.length) + throw new Error('Denominations and respective counts do not match!') - const virtualCassettes = denominations.length ? [Math.max(...denominations) * 2] : [] + const cassettes = _.map( + it => ({ + name: `cassette${it + 1}`, + denomination: parseInt(denominations[it], 10), + count: parseInt(counts[it], 10), + }), + _.range(0, numberOfCassettes), + ) - try { - return { - cassettes: computeAvailableCassettes(cassettes, redeemableTxs), - virtualCassettes - } - } catch (err) { - logger.error(err) - return { - cassettes, - virtualCassettes - } + const virtualCassettes = denominations.length + ? [Math.max(...denominations) * 2] + : [] + + try { + return { + cassettes: computeAvailableCassettes(cassettes, redeemableTxs), + virtualCassettes, } - }) + } catch (err) { + logger.error(err) + return { + cassettes, + virtualCassettes, + } + } + }) } - function buildAvailableRecyclers (excludeTxId) { + function buildAvailableRecyclers(excludeTxId) { const cashOutConfig = configManager.getCashOut(deviceId, settings.config) if (!cashOutConfig.active) return Promise.resolve() - return Promise.all([dbm.recyclerCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)]) - .then(([{ counts, numberOfRecyclers }, redeemableTxs]) => { - redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), redeemableTxs) + return Promise.all([ + dbm.recyclerCounts(deviceId), + cashOutHelper.redeemableTxs(deviceId, excludeTxId), + ]).then(([{ counts, numberOfRecyclers }, redeemableTxs]) => { + redeemableTxs = _.reject( + _.matchesProperty('id', excludeTxId), + redeemableTxs, + ) - const denominations = _.map( - it => cashOutConfig[`recycler${it}`], - _.range(1, numberOfRecyclers+1) - ) + const denominations = _.map( + it => cashOutConfig[`recycler${it}`], + _.range(1, numberOfRecyclers + 1), + ) - if (counts.length !== denominations.length) - throw new Error('Denominations and respective counts do not match!') + if (counts.length !== denominations.length) + throw new Error('Denominations and respective counts do not match!') - const recyclers = _.map( - it => ({ - number: it + 1, - name: `recycler${it + 1}`, - denomination: parseInt(denominations[it], 10), - count: parseInt(counts[it], 10) - }), - _.range(0, numberOfRecyclers) - ) + const recyclers = _.map( + it => ({ + number: it + 1, + name: `recycler${it + 1}`, + denomination: parseInt(denominations[it], 10), + count: parseInt(counts[it], 10), + }), + _.range(0, numberOfRecyclers), + ) - const virtualRecyclers = denominations.length ? [Math.max(..._.flatten(denominations)) * 2] : [] + const virtualRecyclers = denominations.length + ? [Math.max(..._.flatten(denominations)) * 2] + : [] - try { - return { - recyclers: computeAvailableRecyclers(recyclers, redeemableTxs), - virtualRecyclers - } - } catch (err) { - logger.error(err) - return { - recyclers, - virtualRecyclers - } + try { + return { + recyclers: computeAvailableRecyclers(recyclers, redeemableTxs), + virtualRecyclers, } - }) + } catch (err) { + logger.error(err) + return { + recyclers, + virtualRecyclers, + } + } + }) } - function buildAvailableUnits (excludeTxId) { - return Promise.all([buildAvailableCassettes(excludeTxId), buildAvailableRecyclers(excludeTxId)]) - .then(([cassettes, recyclers]) => ({ cassettes: cassettes.cassettes, recyclers: recyclers.recyclers })) + function buildAvailableUnits(excludeTxId) { + return Promise.all([ + buildAvailableCassettes(excludeTxId), + buildAvailableRecyclers(excludeTxId), + ]).then(([cassettes, recyclers]) => ({ + cassettes: cassettes.cassettes, + recyclers: recyclers.recyclers, + })) } - function mapCoinSettings (coinParams) { - const [ cryptoCode, cryptoNetwork ] = coinParams - const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + function mapCoinSettings(coinParams) { + const [cryptoCode, cryptoNetwork] = coinParams + const commissions = configManager.getCommissions( + cryptoCode, + deviceId, + settings.config, + ) const minimumTx = new BN(commissions.minimumTx) const cashInFee = new BN(commissions.fixedFee) const cashOutFee = new BN(commissions.cashOutFixedFee) const cashInCommission = new BN(commissions.cashIn) - const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null + const cashOutCommission = _.isNumber(commissions.cashOut) + ? new BN(commissions.cashOut) + : null const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) - const cryptoUnits = configManager.getCryptoUnits(cryptoCode, settings.config) + const cryptoUnits = configManager.getCryptoUnits( + cryptoCode, + settings.config, + ) return { cryptoCode, @@ -266,24 +319,30 @@ function plugins (settings, deviceId) { cashInCommission, cashOutCommission, cryptoNetwork, - cryptoUnits + cryptoUnits, } } - function getTickerRates (fiatCode, cryptoCode) { + function getTickerRates(fiatCode, cryptoCode) { return ticker.getRates(settings, fiatCode, cryptoCode) } - function pollQueries () { + function pollQueries() { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies - const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config) + const machineScreenOpts = configManager.getAllMachineScreenOpts( + settings.config, + ) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) - const networkPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c)) - const supportsBatchingPromise = cryptoCodes.map(c => wallet.supportsBatching(settings, c)) + const networkPromises = cryptoCodes.map(c => + wallet.cryptoNetwork(settings, c), + ) + const supportsBatchingPromise = cryptoCodes.map(c => + wallet.supportsBatching(settings, c), + ) return Promise.all([ buildAvailableCassettes(), @@ -294,9 +353,9 @@ function plugins (settings, deviceId) { Promise.all(supportsBatchingPromise), Promise.all(tickerPromises), Promise.all(balancePromises), - Promise.all(networkPromises) - ]) - .then(([ + Promise.all(networkPromises), + ]).then( + ([ cassettes, recyclers, configVersion, @@ -305,19 +364,16 @@ function plugins (settings, deviceId) { batchableCoins, tickers, balances, - networks + networks, ]) => { const coinsWithoutRate = _.flow( _.zip(cryptoCodes), - _.map(mapCoinSettings) + _.map(mapCoinSettings), )(networks) const coins = _.flow( _.map(it => ({ batchable: it })), - _.zipWith( - _.assign, - _.zipWith(_.assign, coinsWithoutRate, tickers) - ) + _.zipWith(_.assign, _.zipWith(_.assign, coinsWithoutRate, tickers)), )(batchableCoins) return { @@ -329,195 +385,219 @@ function plugins (settings, deviceId) { configVersion, areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, timezone, - screenOptions: machineScreenOpts + screenOptions: machineScreenOpts, } - }) + }, + ) } - function sendCoins (tx) { - return wallet.supportsBatching(settings, tx.cryptoCode) + function sendCoins(tx) { + return wallet + .supportsBatching(settings, tx.cryptoCode) .then(supportsBatching => { if (supportsBatching) { - return transactionBatching.addTransactionToBatch(tx) - .then(() => ({ - batched: true, - sendPending: false, - error: null, - errorCode: null - })) + return transactionBatching.addTransactionToBatch(tx).then(() => ({ + batched: true, + sendPending: false, + error: null, + errorCode: null, + })) } return wallet.sendCoins(settings, tx) }) } - function recordPing (deviceTime, version, model) { + function recordPing(deviceTime, version, model) { const devices = { version, model, - last_online: deviceTime + last_online: deviceTime, } return Promise.all([ - db.none(`insert into machine_pings(device_id, device_time) values($1, $2) - ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime]), - db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', { - deviceId - }) + db.none( + `insert into machine_pings(device_id, device_time) + values ($1, $2) + ON CONFLICT (device_id) DO UPDATE SET device_time = $2, + updated = now()`, + [deviceId, deviceTime], + ), + db.none( + pgp.helpers.update(devices, null, 'devices') + + 'WHERE device_id = ${deviceId}', + { + deviceId, + }, + ), ]) } - function pruneMachinesHeartbeat () { - const sql = `DELETE FROM machine_network_heartbeat h - USING (SELECT device_id, max(created) as lastEntry FROM machine_network_heartbeat GROUP BY device_id) d - WHERE d.device_id = h.device_id AND h.created < d.lastEntry` + function pruneMachinesHeartbeat() { + const sql = `DELETE + FROM machine_network_heartbeat h + USING (SELECT device_id, max(created) as lastEntry + FROM machine_network_heartbeat + GROUP BY device_id) d + WHERE d.device_id = h.device_id + AND h.created < d.lastEntry` return db.none(sql) } - function isHd (tx) { + function isHd(tx) { return wallet.isHd(settings, tx) } - function getStatus (tx) { - return wallet.getStatus(settings, tx, deviceId) + function getStatus(tx) { + return wallet.getStatus(settings, tx) } - function newAddress (tx) { + function newAddress(tx) { const info = { cryptoCode: tx.cryptoCode, label: 'TX ' + Date.now(), account: 'deposit', hdIndex: tx.hdIndex, cryptoAtoms: tx.cryptoAtoms, - isLightning: tx.isLightning + isLightning: tx.isLightning, } return wallet.newAddress(settings, info, tx) } - function fiatBalance (fiatCode, cryptoCode) { - const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + function fiatBalance(fiatCode, cryptoCode) { + const commissions = configManager.getCommissions( + cryptoCode, + deviceId, + settings.config, + ) return Promise.all([ getTickerRates(fiatCode, cryptoCode), - wallet.balance(settings, cryptoCode) - ]) - .then(([rates, balanceRec]) => { - if (!rates || !balanceRec) return null + wallet.balance(settings, cryptoCode), + ]).then(([rates, balanceRec]) => { + if (!rates || !balanceRec) return null - const rawRate = rates.rates.ask - const cashInCommission = new BN(1).minus(new BN(commissions.cashIn).div(100)) - const balance = balanceRec.balance + const rawRate = rates.rates.ask + const cashInCommission = new BN(1).minus( + new BN(commissions.cashIn).div(100), + ) + const balance = balanceRec.balance - if (!rawRate || !balance) return null + if (!rawRate || !balance) return null - const rate = rawRate.div(cashInCommission) + const rate = rawRate.div(cashInCommission) - const lowBalanceMargin = new BN(0.95) + 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).times(lowBalanceMargin) + const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) + const unitScale = cryptoRec.unitScale + const shiftedRate = rate.shiftedBy(-unitScale) + const fiatTransferBalance = balance + .times(shiftedRate) + .times(lowBalanceMargin) - return { - timestamp: balanceRec.timestamp, - balance: fiatTransferBalance.integerValue(BN.ROUND_DOWN).toString() - } - }) + return { + timestamp: balanceRec.timestamp, + balance: fiatTransferBalance.integerValue(BN.ROUND_DOWN).toString(), + } + }) } - function notifyConfirmation (tx) { + function notifyConfirmation(tx) { logger.debug('notifyConfirmation') const phone = tx.phone - const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC` - return sms.getSms(CASH_OUT_DISPENSE_READY, phone, { timestamp }) + const timestamp = `${new Date().toISOString().substring(11, 19)} UTC` + return sms + .getSms(CASH_OUT_DISPENSE_READY, phone, { timestamp }) .then(smsObj => { const rec = { - sms: smsObj + sms: smsObj, } - - return sms.sendMessage(settings, rec) - .then(() => { - const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2' - const values = [true, tx.id] - return db.none(sql, values) - }) + return sms.sendMessage(settings, rec).then(() => { + const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2' + const values = [true, tx.id] + + return db.none(sql, values) + }) }) } - function notifyOperator (tx, rec) { + function notifyOperator(tx, rec) { // notify operator about new transaction and add high volume txs to database return notifier.transactionNotify(tx, rec) } - function clearOldLogs () { - return logs.clearOldLogs() - .catch(logger.error) + function clearOldLogs() { + return logs.clearOldLogs().catch(logger.error) } /* * Trader functions */ - function toMarketString (fiatCode, cryptoCode) { + function toMarketString(fiatCode, cryptoCode) { return [fiatCode, cryptoCode].join('-') } - - function fromMarketString (market) { + + function fromMarketString(market) { const [fiatCode, cryptoCode] = market.split('-') return { fiatCode, cryptoCode } } - function buy (rec, tx) { + function buy(rec, tx) { return buyAndSell(rec, true, tx) } - function sell (rec) { + function sell(rec) { return buyAndSell(rec, false) } - function buyAndSell (rec, doBuy, tx) { + function buyAndSell(rec, doBuy, tx) { const cryptoCode = rec.cryptoCode if (!exchange.active(settings, cryptoCode)) return - return exchange.fetchExchange(settings, cryptoCode) - .then(_exchange => { - const fiatCode = _exchange.account.currencyMarket - const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() + return exchange.fetchExchange(settings, cryptoCode).then(_exchange => { + const fiatCode = _exchange.account.currencyMarket + const cryptoAtoms = doBuy + ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) + : rec.cryptoAtoms.negated() - const market = toMarketString(fiatCode, cryptoCode) + const market = toMarketString(fiatCode, cryptoCode) - const direction = doBuy ? 'cashIn' : 'cashOut' - const internalTxId = tx ? tx.id : rec.id - logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) - if (!tradesQueues[market]) tradesQueues[market] = [] - tradesQueues[market].push({ - direction, - internalTxId, - fiatCode, - cryptoAtoms, - cryptoCode, - timestamp: Date.now() - }) + const direction = doBuy ? 'cashIn' : 'cashOut' + const internalTxId = tx ? tx.id : rec.id + logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) + if (!tradesQueues[market]) tradesQueues[market] = [] + tradesQueues[market].push({ + direction, + internalTxId, + fiatCode, + cryptoAtoms, + cryptoCode, + timestamp: Date.now(), }) + }) } - function consolidateTrades (cryptoCode, fiatCode) { + function consolidateTrades(cryptoCode, fiatCode) { const market = toMarketString(fiatCode, cryptoCode) const marketTradesQueues = tradesQueues[market] if (!marketTradesQueues || marketTradesQueues.length === 0) return null - logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length) + logger.debug( + '[%s] tradesQueues size: %d', + market, + marketTradesQueues.length, + ) logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0]) const t1 = Date.now() - const filtered = marketTradesQueues - .filter(tradeEntry => { - return t1 - tradeEntry.timestamp < TRADE_TTL - }) + const filtered = marketTradesQueues.filter(tradeEntry => { + return t1 - tradeEntry.timestamp < TRADE_TTL + }) const filteredCount = marketTradesQueues.length - filtered.length @@ -528,13 +608,22 @@ function plugins (settings, deviceId) { if (filtered.length === 0) return null - const partitionByDirection = _.partition(({ direction }) => direction === 'cashIn') - const [cashInTxs, cashOutTxs] = _.compose(partitionByDirection, _.uniqBy('internalTxId'))(filtered) + const partitionByDirection = _.partition( + ({ direction }) => direction === 'cashIn', + ) + const [cashInTxs, cashOutTxs] = _.compose( + partitionByDirection, + _.uniqBy('internalTxId'), + )(filtered) - const cryptoAtoms = filtered - .reduce((prev, current) => prev.plus(current.cryptoAtoms), new BN(0)) + const cryptoAtoms = filtered.reduce( + (prev, current) => prev.plus(current.cryptoAtoms), + new BN(0), + ) - const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0) + const timestamp = filtered + .map(r => r.timestamp) + .reduce((acc, r) => Math.max(acc, r), 0) const consolidatedTrade = { cashInTxs, @@ -542,7 +631,7 @@ function plugins (settings, deviceId) { fiatCode, cryptoAtoms, cryptoCode, - timestamp + timestamp, } tradesQueues[market] = [] @@ -551,7 +640,7 @@ function plugins (settings, deviceId) { return consolidatedTrade } - function executeTrades () { + function executeTrades() { const pairs = _.map(fromMarketString)(_.keys(tradesQueues)) pairs.forEach(({ fiatCode, cryptoCode }) => { try { @@ -565,7 +654,7 @@ function plugins (settings, deviceId) { return Promise.resolve() } - function executeTradesForMarket (settings, fiatCode, cryptoCode) { + function executeTradesForMarket(settings, fiatCode, cryptoCode) { if (!exchange.active(settings, cryptoCode)) return const market = toMarketString(fiatCode, cryptoCode) @@ -573,47 +662,53 @@ function plugins (settings, deviceId) { if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return - return executeTradeForType(tradeEntry) - .catch(err => { - tradesQueues[market].push(tradeEntry) - if (err.name === 'orderTooSmall') return logger.debug(err.message) - logger.error(err) - }) + return executeTradeForType(tradeEntry).catch(err => { + tradesQueues[market].push(tradeEntry) + if (err.name === 'orderTooSmall') return logger.debug(err.message) + logger.error(err) + }) } - function executeTradeForType (_tradeEntry) { - const expand = te => _.assign(te, { - cryptoAtoms: te.cryptoAtoms.abs(), - type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell' - }) + function executeTradeForType(_tradeEntry) { + const expand = te => + _.assign(te, { + cryptoAtoms: te.cryptoAtoms.abs(), + type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell', + }) const tradeEntry = expand(_tradeEntry) const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell - return recordTrade(tradeEntry) - .then(newEntry => { - tradeEntry.tradeId = newEntry.id - return execute(settings, tradeEntry) - .catch(err => { - updateTradeEntry(tradeEntry, newEntry, err) - .then(() => { - logger.error(err) - throw err - }) - }) + return recordTrade(tradeEntry).then(newEntry => { + tradeEntry.tradeId = newEntry.id + return execute(settings, tradeEntry).catch(err => { + updateTradeEntry(tradeEntry, newEntry, err).then(() => { + logger.error(err) + throw err + }) }) + }) } - function updateTradeEntry (tradeEntry, newEntry, err) { + function updateTradeEntry(tradeEntry, newEntry, err) { const data = mergeTradeEntryAndError(tradeEntry, err) - const sql = pgp.helpers.update(data, ['error'], 'trades') + ` WHERE id = ${newEntry.id}` + const sql = + pgp.helpers.update(data, ['error'], 'trades') + + ` WHERE id = ${newEntry.id}` return db.none(sql) } - function recordTradeAndTx (tradeId, { cashInTxs, cashOutTxs }, dbTx) { - const columnSetCashIn = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashin_tx_trades' }) - const columnSetCashOut = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashout_tx_trades' }) - const mapToEntry = _.map(tx => ({ tx_id: tx.internalTxId, trade_id: tradeId })) + function recordTradeAndTx(tradeId, { cashInTxs, cashOutTxs }, dbTx) { + const columnSetCashIn = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { + table: 'cashin_tx_trades', + }) + const columnSetCashOut = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { + table: 'cashout_tx_trades', + }) + const mapToEntry = _.map(tx => ({ + tx_id: tx.internalTxId, + trade_id: tradeId, + })) const queries = [] if (!_.isEmpty(cashInTxs)) { @@ -627,89 +722,100 @@ function plugins (settings, deviceId) { return Promise.all(queries) } - function convertBigNumFields (obj) { - const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat']) - ? value.toString() - : value + function convertBigNumFields(obj) { + const convert = (value, key) => + _.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : value - const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat']) - ? key + '#' - : key + const convertKey = key => + _.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key return _.mapKeys(convertKey, mapValuesWithKey(convert, obj)) } - function mergeTradeEntryAndError (tradeEntry, error) { + function mergeTradeEntryAndError(tradeEntry, error) { if (error && error.message) { return Object.assign({}, tradeEntry, { - error: error.message.slice(0, 200) + error: error.message.slice(0, 200), }) } return tradeEntry } - function recordTrade (_tradeEntry, error) { + function recordTrade(_tradeEntry, error) { const massage = _.flow( mergeTradeEntryAndError, _.pick(['cryptoCode', 'cryptoAtoms', 'fiatCode', 'type', 'error']), convertBigNumFields, - _.mapKeys(_.snakeCase) + _.mapKeys(_.snakeCase), ) const tradeEntry = massage(_tradeEntry, error) const sql = pgp.helpers.insert(tradeEntry, null, 'trades') + 'RETURNING *' return db.tx(t => { - return t.oneOrNone(sql) - .then(newTrade => { - return recordTradeAndTx(newTrade.id, _tradeEntry, t) - .then(() => newTrade) - }) + return t.oneOrNone(sql).then(newTrade => { + return recordTradeAndTx(newTrade.id, _tradeEntry, t).then( + () => newTrade, + ) + }) }) } - function sendMessage (rec) { + function sendMessage(rec) { const notifications = configManager.getGlobalNotifications(settings.config) let promises = [] - if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec)) - if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec)) + if (notifications.email.active && rec.email) + promises.push(email.sendMessage(settings, rec)) + if (notifications.sms.active && rec.sms) + promises.push(sms.sendMessage(settings, rec)) return Promise.all(promises) } - function checkDevicesCashBalances (fiatCode, devices) { + function checkDevicesCashBalances(fiatCode, devices) { return _.map(device => checkDeviceCashBalances(fiatCode, device), devices) } - function getCashUnitCapacity (model, device) { + function getCashUnitCapacity(model, device) { if (!CASH_UNIT_CAPACITY[model]) { return CASH_UNIT_CAPACITY.default[device] } return CASH_UNIT_CAPACITY[model][device] } - function checkDeviceCashBalances (fiatCode, device) { + function checkDeviceCashBalances(fiatCode, device) { const deviceId = device.deviceId const machineName = device.name - const notifications = configManager.getNotifications(null, deviceId, settings.config) + const notifications = configManager.getNotifications( + null, + deviceId, + settings.config, + ) - const cashInAlerts = device.cashUnits.cashbox > notifications.cashInAlertThreshold - ? [{ - code: 'CASH_BOX_FULL', - machineName, - deviceId, - notes: device.cashUnits.cashbox - }] - : [] + const cashInAlerts = + device.cashUnits.cashbox > notifications.cashInAlertThreshold + ? [ + { + code: 'CASH_BOX_FULL', + machineName, + deviceId, + notes: device.cashUnits.cashbox, + }, + ] + : [] const cashOutConfig = configManager.getCashOut(deviceId, settings.config) const cashOutEnabled = cashOutConfig.active - const isUnitLow = (have, max, limit) => ((have / max) * 100) < limit + const isUnitLow = (have, max, limit) => (have / max) * 100 < limit - if (!cashOutEnabled) - return cashInAlerts + if (!cashOutEnabled) return cashInAlerts const cassetteCapacity = getCashUnitCapacity(device.model, 'cassette') - const cassetteAlerts = Array(Math.min(device.numberOfCassettes ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES)) + const cassetteAlerts = Array( + Math.min( + device.numberOfCassettes ?? 0, + CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, + ), + ) .fill(null) .flatMap((_elem, idx) => { const nth = idx + 1 @@ -718,21 +824,28 @@ function plugins (settings, deviceId) { const denomination = cashOutConfig[cassetteField] const limit = notifications[`fillingPercentageCassette${nth}`] - return isUnitLow(notes, cassetteCapacity, limit) ? - [{ - code: 'LOW_CASH_OUT', - cassette: nth, - machineName, - deviceId, - notes, - denomination, - fiatCode - }] : - [] + return isUnitLow(notes, cassetteCapacity, limit) + ? [ + { + code: 'LOW_CASH_OUT', + cassette: nth, + machineName, + deviceId, + notes, + denomination, + fiatCode, + }, + ] + : [] }) const recyclerCapacity = getCashUnitCapacity(device.model, 'recycler') - const recyclerAlerts = Array(Math.min(device.numberOfRecyclers ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS)) + const recyclerAlerts = Array( + Math.min( + device.numberOfRecyclers ?? 0, + CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS, + ), + ) .fill(null) .flatMap((_elem, idx) => { const nth = idx + 1 @@ -741,24 +854,27 @@ function plugins (settings, deviceId) { const denomination = cashOutConfig[recyclerField] const limit = notifications[`fillingPercentageRecycler${nth}`] - return isUnitLow(notes, recyclerCapacity, limit) ? - [{ - code: 'LOW_RECYCLER_STACKER', - cassette: nth, // @see DETAIL_TEMPLATE in /lib/notifier/utils.js - machineName, - deviceId, - notes, - denomination, - fiatCode - }] : - [] + return isUnitLow(notes, recyclerCapacity, limit) + ? [ + { + code: 'LOW_RECYCLER_STACKER', + cassette: nth, // @see DETAIL_TEMPLATE in /lib/notifier/utils.js + machineName, + deviceId, + notes, + denomination, + fiatCode, + }, + ] + : [] }) return [].concat(cashInAlerts, cassetteAlerts, recyclerAlerts) } - function checkCryptoBalances (fiatCode, devices) { - const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes) + function checkCryptoBalances(fiatCode, devices) { + const fiatBalancePromises = cryptoCodes => + _.map(c => fiatBalance(fiatCode, c), cryptoCodes) const fetchCryptoCodes = _deviceId => { const localeConfig = configManager.getLocale(_deviceId, settings.config) @@ -769,117 +885,149 @@ function plugins (settings, deviceId) { const cryptoCodes = union(devices) const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode]) - return Promise.all(fiatBalancePromises(cryptoCodes)) - .then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances))) + return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances => + _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)), + ) } - function checkCryptoBalance (fiatCode, rec) { + function checkCryptoBalance(fiatCode, rec) { const [cryptoCode, fiatBalance] = rec if (!fiatBalance) return null - const notifications = configManager.getNotifications(cryptoCode, null, settings.config) + const notifications = configManager.getNotifications( + cryptoCode, + null, + settings.config, + ) const lowAlertThreshold = notifications.cryptoLowBalance const highAlertThreshold = notifications.cryptoHighBalance const req = { cryptoCode, fiatBalance, - fiatCode + fiatCode, } - if (_.isFinite(lowAlertThreshold) && new BN(fiatBalance.balance).lt(lowAlertThreshold)) { + if ( + _.isFinite(lowAlertThreshold) && + new BN(fiatBalance.balance).lt(lowAlertThreshold) + ) { return _.set('code')('LOW_CRYPTO_BALANCE')(req) } - if (_.isFinite(highAlertThreshold) && new BN(fiatBalance.balance).gt(highAlertThreshold)) { + if ( + _.isFinite(highAlertThreshold) && + new BN(fiatBalance.balance).gt(highAlertThreshold) + ) { return _.set('code')('HIGH_CRYPTO_BALANCE')(req) } return null } - function checkBalances () { + function checkBalances() { const localeConfig = configManager.getGlobalLocale(settings.config) const fiatCode = localeConfig.fiatCurrency - return machineLoader.getMachines() - .then(devices => Promise.all([ - checkCryptoBalances(fiatCode, devices), - checkDevicesCashBalances(fiatCode, devices) - ])) + return machineLoader + .getMachines() + .then(devices => + Promise.all([ + checkCryptoBalances(fiatCode, devices), + checkDevicesCashBalances(fiatCode, devices), + ]), + ) .then(_.flow(_.flattenDeep, _.compact)) } - function randomCode () { - return new BN(crypto.randomBytes(3).toString('hex'), 16).shiftedBy(-6).toFixed(6).slice(-6) + function randomCode() { + return new BN(crypto.randomBytes(3).toString('hex'), 16) + .shiftedBy(-6) + .toFixed(6) + .slice(-6) } - function getPhoneCode (phone) { - const code = settings.config.notifications_thirdParty_sms === 'mock-sms' - ? '123' - : randomCode() + function getPhoneCode(phone) { + const code = + settings.config.notifications_thirdParty_sms === 'mock-sms' + ? '123' + : randomCode() - const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC` - return sms.getSms(CONFIRMATION_CODE, phone, { code, timestamp }) + const timestamp = `${new Date().toISOString().substring(11, 19)} UTC` + return sms + .getSms(CONFIRMATION_CODE, phone, { code, timestamp }) .then(smsObj => { const rec = { - sms: smsObj + sms: smsObj, } - return sms.sendMessage(settings, rec) - .then(() => code) + return sms.sendMessage(settings, rec).then(() => code) }) } - function getEmailCode (toEmail) { - const code = settings.config.notifications_thirdParty_email === 'mock-email' - ? '123' - : randomCode() + function getEmailCode(toEmail) { + const code = + settings.config.notifications_thirdParty_email === 'mock-email' + ? '123' + : randomCode() const rec = { email: { toEmail, subject: 'Your cryptomat code', - body: `Your cryptomat code: ${code}` - } + body: `Your cryptomat code: ${code}`, + }, } - return email.sendCustomerMessage(settings, rec) - .then(() => code) + return email.sendCustomerMessage(settings, rec).then(() => code) } - function sweepHdRow (row) { + function sweepHdRow(row) { const txId = row.id const cryptoCode = row.crypto_code - return wallet.sweep(settings, txId, cryptoCode, row.hd_index) + return wallet + .sweep(settings, txId, cryptoCode, row.hd_index) .then(txHash => { if (txHash) { logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash) - const sql = `update cash_out_txs set swept='t' - where id=$1` + const sql = `update cash_out_txs + set swept='t' + where id = $1` return db.none(sql, row.id) } }) - .catch(err => logger.error('[%s] [Session ID: %s] Sweep error: %s', cryptoCode, row.id, err.message)) + .catch(err => + logger.error( + '[%s] [Session ID: %s] Sweep error: %s', + cryptoCode, + row.id, + err.message, + ), + ) } - function sweepHd () { - const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs - WHERE hd_index IS NOT NULL AND NOT swept AND status IN ('confirmed', 'instant') AND created > now() - interval '1 week'` + function sweepHd() { + const sql = `SELECT id, crypto_code, hd_index + FROM cash_out_txs + WHERE hd_index IS NOT NULL + AND NOT swept + AND status IN ('confirmed', 'instant') + AND created > now() - interval '1 week'` - return db.any(sql) + return db + .any(sql) .then(rows => Promise.all(rows.map(sweepHdRow))) .catch(logger.error) } - function getMachineNames () { + function getMachineNames() { return machineLoader.getMachineNames(settings.config) } - function getRawRates () { + function getRawRates() { const localeConfig = configManager.getGlobalLocale(settings.config) const fiatCode = localeConfig.fiatCurrency @@ -889,24 +1037,23 @@ function plugins (settings, deviceId) { return Promise.all(tickerPromises) } - function getRates () { - return getRawRates() - .then(buildRates) + function getRates() { + return getRawRates().then(buildRates) } - function rateAddress (cryptoCode, address) { + function rateAddress(cryptoCode, address) { return walletScoring.rateAddress(settings, cryptoCode, address) } - function rateTransaction (cryptoCode, address) { + function rateTransaction(cryptoCode, address) { return walletScoring.rateTransaction(settings, cryptoCode, address) } - function isWalletScoringEnabled (tx) { + function isWalletScoringEnabled(tx) { return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode) } - function probeLN (cryptoCode, address) { + function probeLN(cryptoCode, address) { return wallet.probeLN(settings, cryptoCode, address) } @@ -940,7 +1087,7 @@ function plugins (settings, deviceId) { rateTransaction, isWalletScoringEnabled, probeLN, - buildAvailableUnits + buildAvailableUnits, } } diff --git a/packages/server/lib/plugins/common/ccxt.js b/packages/server/lib/plugins/common/ccxt.js index 247ea7b1..a60eb63a 100644 --- a/packages/server/lib/plugins/common/ccxt.js +++ b/packages/server/lib/plugins/common/ccxt.js @@ -1,51 +1,53 @@ -const { COINS } = require('@lamassu/coins') -const _ = require('lodash/fp') -const { utils: coinUtils } = require('@lamassu/coins') - -const kraken = require('../exchange/kraken') -const bitstamp = require('../exchange/bitstamp') -const itbit = require('../exchange/itbit') -const binanceus = require('../exchange/binanceus') -const cex = require('../exchange/cex') -const bitpay = require('../ticker/bitpay') -const binance = require('../exchange/binance') -const bitfinex = require('../exchange/bitfinex') -const logger = require('../../logger') - -const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, TRX, USDT_TRON, LN, USDC } = COINS - -const ALL = { - cex: cex, - binanceus: binanceus, - kraken: kraken, - bitstamp: bitstamp, - itbit: itbit, - bitpay: bitpay, - binance: binance, - bitfinex: bitfinex -} - -function buildMarket (fiatCode, cryptoCode, serviceName) { - if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { - throw new Error('Unsupported crypto: ' + cryptoCode) - } - - if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code') - return cryptoCode + '/' + fiatCode -} - -function verifyFiatSupport (fiatCode, serviceName) { - const fiat = ALL[serviceName].FIAT - return fiat === 'ALL_CURRENCIES' ? true : _.includes(fiatCode, fiat) -} - -function isConfigValid (config, fields) { - const values = _.map(it => _.get(it)(config))(fields) - return _.every(it => it || it === 0)(values) -} - -function defaultFiatMarket (serviceName) { - return ALL[serviceName].DEFAULT_FIAT_MARKET -} - -module.exports = { buildMarket, ALL, verifyFiatSupport, isConfigValid, defaultFiatMarket } +const _ = require('lodash/fp') + +const kraken = require('../exchange/kraken') +const bitstamp = require('../exchange/bitstamp') +const itbit = require('../exchange/itbit') +const binanceus = require('../exchange/binanceus') +const cex = require('../exchange/cex') +const bitpay = require('../ticker/bitpay') +const binance = require('../exchange/binance') +const bitfinex = require('../exchange/bitfinex') + +const ALL = { + cex: cex, + binanceus: binanceus, + kraken: kraken, + bitstamp: bitstamp, + itbit: itbit, + bitpay: bitpay, + binance: binance, + bitfinex: bitfinex, +} + +function buildMarket(fiatCode, cryptoCode, serviceName) { + if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { + throw new Error('Unsupported crypto: ' + cryptoCode) + } + + if (_.isNil(fiatCode)) + throw new Error('Market pair building failed: Missing fiat code') + return cryptoCode + '/' + fiatCode +} + +function verifyFiatSupport(fiatCode, serviceName) { + const fiat = ALL[serviceName].FIAT + return fiat === 'ALL_CURRENCIES' ? true : _.includes(fiatCode, fiat) +} + +function isConfigValid(config, fields) { + const values = _.map(it => _.get(it)(config))(fields) + return _.every(it => it || it === 0)(values) +} + +function defaultFiatMarket(serviceName) { + return ALL[serviceName].DEFAULT_FIAT_MARKET +} + +module.exports = { + buildMarket, + ALL, + verifyFiatSupport, + isConfigValid, + defaultFiatMarket, +} diff --git a/packages/server/lib/plugins/common/json-rpc.js b/packages/server/lib/plugins/common/json-rpc.js index 41a5ef37..b0e22361 100644 --- a/packages/server/lib/plugins/common/json-rpc.js +++ b/packages/server/lib/plugins/common/json-rpc.js @@ -7,17 +7,19 @@ const request = require('request-promise') const { utils: coinUtils } = require('@lamassu/coins') const logger = require('../../logger') -const { isRemoteNode, isRemoteWallet } = require('../../environment-helper') +const { isRemoteWallet } = require('../../environment-helper') const { isEnvironmentValid } = require('../../blockchain/install') const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR - module.exports = { - fetch, fetchDigest, parseConf, rpcConfig + fetch, + fetchDigest, + parseConf, + rpcConfig, } -function fetch (account = {}, method, params) { +function fetch(account = {}, method, params) { params = _.defaultTo([], params) return Promise.resolve(true) @@ -25,18 +27,22 @@ function fetch (account = {}, method, params) { const data = { method, params, - id: uuid.v4() + id: uuid.v4(), } - if (_.isNil(account.port)) throw new Error('port attribute required for jsonRpc') + if (_.isNil(account.port)) + throw new Error('port attribute required for jsonRpc') - const url = _.defaultTo(`http://${account.host}:${account.port}`, account.url) + const url = _.defaultTo( + `http://${account.host}:${account.port}`, + account.url, + ) return axios({ method: 'post', - auth: {username: account.username, password: account.password}, + auth: { username: account.username, password: account.password }, url, - data + data, }) }) .then(r => { @@ -44,17 +50,19 @@ function fetch (account = {}, method, params) { return r.data.result }) .catch(err => { - throw new Error(JSON.stringify({ - responseMessage: _.get('message', err), - message: _.get('response.data.error.message', err), - code: _.get('response.data.error.code', err) - })) + throw new Error( + JSON.stringify({ + responseMessage: _.get('message', err), + message: _.get('response.data.error.message', err), + code: _.get('response.data.error.code', err), + }), + ) }) } -function generateDigestOptions (account = {}, method, params) { +function generateDigestOptions(account = {}, method, params) { const headers = { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', } const dataString = `{"jsonrpc":"2.0","id":"${uuid.v4()}","method":"${method}","params":${JSON.stringify(params)}}` @@ -68,31 +76,30 @@ function generateDigestOptions (account = {}, method, params) { auth: { user: account.username, pass: account.password, - sendImmediately: false - } + sendImmediately: false, + }, } return options } function fetchDigest(account = {}, method, params = []) { - return Promise.resolve(true) - .then(() => { - if (_.isNil(account.port)) - throw new Error('port attribute required for jsonRpc') + return Promise.resolve(true).then(() => { + if (_.isNil(account.port)) + throw new Error('port attribute required for jsonRpc') - const options = generateDigestOptions(account, method, params) - return request(options) - }) + const options = generateDigestOptions(account, method, params) + return request(options) + }) } -function split (str) { +function split(str) { const i = str.indexOf('=') if (i === -1) return [] return [str.slice(0, i), str.slice(i + 1)] } -function parseConf (confPath) { +function parseConf(confPath) { const conf = fs.readFileSync(confPath) const lines = conf.toString().split('\n') @@ -109,34 +116,36 @@ function parseConf (confPath) { return res } -function rpcConfig (cryptoRec) { +function rpcConfig(cryptoRec) { try { if (isRemoteWallet(cryptoRec) && isEnvironmentValid(cryptoRec)) { return { username: process.env[`${cryptoRec.cryptoCode}_NODE_USER`], password: process.env[`${cryptoRec.cryptoCode}_NODE_PASSWORD`], host: process.env[`${cryptoRec.cryptoCode}_NODE_RPC_HOST`], - port: process.env[`${cryptoRec.cryptoCode}_NODE_RPC_PORT`] + port: process.env[`${cryptoRec.cryptoCode}_NODE_RPC_PORT`], } } const configPath = coinUtils.configPath(cryptoRec, BLOCKCHAIN_DIR) const config = parseConf(configPath) - + return { username: config.rpcuser, password: config.rpcpassword, host: 'localhost', - port: config.rpcport || cryptoRec.defaultPort + port: config.rpcport || cryptoRec.defaultPort, } } catch (err) { if (!isEnvironmentValid(cryptoRec)) { - logger.error('Environment is not correctly setup for remote wallet usage!') + logger.error( + `Environment is not correctly setup for remote wallet usage!, ${err}`, + ) } else { logger.error('Wallet is currently not installed!') } return { - port: cryptoRec.defaultPort + port: cryptoRec.defaultPort, } } } diff --git a/packages/server/lib/plugins/compliance/consts.js b/packages/server/lib/plugins/compliance/consts.js index c6291189..3e0cc98d 100644 --- a/packages/server/lib/plugins/compliance/consts.js +++ b/packages/server/lib/plugins/compliance/consts.js @@ -2,5 +2,5 @@ module.exports = { PENDING: 'PENDING', RETRY: 'RETRY', APPROVED: 'APPROVED', - REJECTED: 'REJECTED' -} \ No newline at end of file + REJECTED: 'REJECTED', +} diff --git a/packages/server/lib/plugins/compliance/mock-compliance/mock-compliance.js b/packages/server/lib/plugins/compliance/mock-compliance/mock-compliance.js index 954b8072..c6b4e84a 100644 --- a/packages/server/lib/plugins/compliance/mock-compliance/mock-compliance.js +++ b/packages/server/lib/plugins/compliance/mock-compliance/mock-compliance.js @@ -1,6 +1,6 @@ const uuid = require('uuid') -const {APPROVED} = require('../consts') +const { APPROVED } = require('../consts') const CODE = 'mock-compliance' @@ -8,18 +8,19 @@ const createLink = (settings, userId, level) => { return `this is a mock external link, ${userId}, ${level}` } -const getApplicantStatus = (account, userId) => { +const getApplicantStatus = account => { return Promise.resolve({ service: CODE, status: { - level: account.applicantLevel, answer: APPROVED - } + level: account.applicantLevel, + answer: APPROVED, + }, }) } const createApplicant = () => { return Promise.resolve({ - id: uuid.v4() + id: uuid.v4(), }) } @@ -27,5 +28,5 @@ module.exports = { CODE, createApplicant, getApplicantStatus, - createLink + createLink, } diff --git a/packages/server/lib/plugins/compliance/sumsub/request.js b/packages/server/lib/plugins/compliance/sumsub/request.js index f102f996..518be3b5 100644 --- a/packages/server/lib/plugins/compliance/sumsub/request.js +++ b/packages/server/lib/plugins/compliance/sumsub/request.js @@ -4,7 +4,7 @@ const _ = require('lodash/fp') const FormData = require('form-data') const axiosConfig = { - baseURL: 'https://api.sumsub.com' + baseURL: 'https://api.sumsub.com', } const getSigBuilder = (apiToken, secretKey) => config => { @@ -25,10 +25,13 @@ const getSigBuilder = (apiToken, secretKey) => config => { return config } -const request = ((account, config) => { +const request = (account, config) => { const instance = axios.create(axiosConfig) - instance.interceptors.request.use(getSigBuilder(account.apiToken, account.secretKey), Promise.reject) + instance.interceptors.request.use( + getSigBuilder(account.apiToken, account.secretKey), + Promise.reject, + ) return instance(config) -}) +} module.exports = request diff --git a/packages/server/lib/plugins/compliance/sumsub/sumsub.api.js b/packages/server/lib/plugins/compliance/sumsub/sumsub.api.js index a7d7557a..70ed644e 100644 --- a/packages/server/lib/plugins/compliance/sumsub/sumsub.api.js +++ b/packages/server/lib/plugins/compliance/sumsub/sumsub.api.js @@ -2,7 +2,9 @@ const request = require('./request') const createApplicant = (account, userId, level) => { if (!userId || !level) { - return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`) + return Promise.reject( + `Missing required fields: userId: ${userId}, level: ${level}`, + ) } const config = { @@ -10,12 +12,12 @@ const createApplicant = (account, userId, level) => { url: `/resources/applicants?levelName=${level}`, data: { externalUserId: userId, - sourceKey: 'lamassu' + sourceKey: 'lamassu', }, headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' - } + Accept: 'application/json', + }, } return request(account, config) @@ -23,7 +25,9 @@ const createApplicant = (account, userId, level) => { const createLink = (account, userId, level) => { if (!userId || !level) { - return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`) + return Promise.reject( + `Missing required fields: userId: ${userId}, level: ${level}`, + ) } const config = { @@ -31,8 +35,8 @@ const createLink = (account, userId, level) => { url: `/resources/sdkIntegrations/levels/${level}/websdkLink?ttlInSecs=${600}&externalUserId=${userId}`, headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' - } + Accept: 'application/json', + }, } return request(account, config) @@ -47,9 +51,9 @@ const getApplicantByExternalId = (account, id) => { method: 'GET', url: `/resources/applicants/-;externalUserId=${id}/one`, headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } + Accept: 'application/json', + 'Content-Type': 'application/json', + }, } return request(account, config) @@ -64,9 +68,9 @@ const getApplicantStatus = (account, id) => { method: 'GET', url: `/resources/applicants/${id}/status`, headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } + Accept: 'application/json', + 'Content-Type': 'application/json', + }, } return request(account, config) @@ -81,9 +85,9 @@ const getApplicantById = (account, id) => { method: 'GET', url: `/resources/applicants/${id}/one`, headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } + Accept: 'application/json', + 'Content-Type': 'application/json', + }, } return request(account, config) @@ -94,5 +98,5 @@ module.exports = { createApplicant, getApplicantByExternalId, getApplicantById, - getApplicantStatus + getApplicantStatus, } diff --git a/packages/server/lib/plugins/compliance/sumsub/sumsub.js b/packages/server/lib/plugins/compliance/sumsub/sumsub.js index b295f7c3..1c459c2e 100644 --- a/packages/server/lib/plugins/compliance/sumsub/sumsub.js +++ b/packages/server/lib/plugins/compliance/sumsub/sumsub.js @@ -6,48 +6,53 @@ const { PENDING, RETRY, APPROVED, REJECTED } = require('../consts') const CODE = 'sumsub' const getApplicantByExternalId = (account, userId) => { - return sumsubApi.getApplicantByExternalId(account, userId) - .then(r => r.data) + return sumsubApi.getApplicantByExternalId(account, userId).then(r => r.data) } const createApplicant = (account, userId, level) => { - return sumsubApi.createApplicant(account, userId, level) + return sumsubApi + .createApplicant(account, userId, level) .then(r => r.data) .catch(err => { - if (err.response.status === 409) return getApplicantByExternalId(account, userId) + if (err.response.status === 409) + return getApplicantByExternalId(account, userId) throw err }) } const createLink = (account, userId, level) => { - return sumsubApi.createLink(account, userId, level) - .then(r => r.data.url) + return sumsubApi.createLink(account, userId, level).then(r => r.data.url) } const getApplicantStatus = (account, userId) => { - return sumsubApi.getApplicantByExternalId(account, userId) - .then(r => { - const levelName = _.get('data.review.levelName', r) - const reviewStatus = _.get('data.review.reviewStatus', r) - const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r) - const reviewRejectType = _.get('data.review.reviewResult.reviewRejectType', r) + return sumsubApi.getApplicantByExternalId(account, userId).then(r => { + const levelName = _.get('data.review.levelName', r) + const reviewStatus = _.get('data.review.reviewStatus', r) + const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r) + const reviewRejectType = _.get( + 'data.review.reviewResult.reviewRejectType', + r, + ) - // if last review was from a different level, return the current level and RETRY - if (levelName !== account.applicantLevel) return { level: account.applicantLevel, answer: RETRY } + // if last review was from a different level, return the current level and RETRY + if (levelName !== account.applicantLevel) + return { level: account.applicantLevel, answer: RETRY } - let answer = PENDING - if (reviewStatus === 'init') answer = RETRY - if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') answer = APPROVED - if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY - if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED + let answer = PENDING + if (reviewStatus === 'init') answer = RETRY + if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') + answer = APPROVED + if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY + if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') + answer = REJECTED - return { level: levelName, answer } - }) + return { level: levelName, answer } + }) } module.exports = { CODE, createApplicant, getApplicantStatus, - createLink -} \ No newline at end of file + createLink, +} diff --git a/packages/server/lib/plugins/email/mailgun/mailgun.js b/packages/server/lib/plugins/email/mailgun/mailgun.js index 2bd32151..98bf2c41 100644 --- a/packages/server/lib/plugins/email/mailgun/mailgun.js +++ b/packages/server/lib/plugins/email/mailgun/mailgun.js @@ -2,29 +2,29 @@ const Mailgun = require('mailgun-js') const NAME = 'Mailgun' -function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) { - const mailgun = Mailgun({apiKey, domain}) +function sendMessage({ apiKey, domain, fromEmail, toEmail }, rec) { + const mailgun = Mailgun({ apiKey, domain }) const to = rec.email.toEmail ?? toEmail const emailData = { from: `Lamassu Server ${fromEmail}`, to, subject: rec.email.subject, - text: rec.email.body + text: rec.email.body, } return mailgun.messages().send(emailData) } -function sendCustomerMessage ({apiKey, domain, fromEmail}, rec) { - const mailgun = Mailgun({apiKey, domain}) +function sendCustomerMessage({ apiKey, domain, fromEmail }, rec) { + const mailgun = Mailgun({ apiKey, domain }) const to = rec.email.toEmail const emailData = { from: fromEmail, to, subject: rec.email.subject, - text: rec.email.body + text: rec.email.body, } return mailgun.messages().send(emailData) @@ -33,5 +33,5 @@ function sendCustomerMessage ({apiKey, domain, fromEmail}, rec) { module.exports = { NAME, sendMessage, - sendCustomerMessage + sendCustomerMessage, } diff --git a/packages/server/lib/plugins/email/mock-email/mock-email.js b/packages/server/lib/plugins/email/mock-email/mock-email.js index 219e7dea..0f7cdad4 100644 --- a/packages/server/lib/plugins/email/mock-email/mock-email.js +++ b/packages/server/lib/plugins/email/mock-email/mock-email.js @@ -1,6 +1,6 @@ const NAME = 'mock-email' -function sendMessage (settings, rec) { +function sendMessage(settings, rec) { console.log('sending email', rec) } @@ -11,5 +11,5 @@ function sendCustomerMessage(settings, rec) { module.exports = { NAME, sendMessage, - sendCustomerMessage + sendCustomerMessage, } diff --git a/packages/server/lib/plugins/exchange/binance.js b/packages/server/lib/plugins/exchange/binance.js index dc2a4c52..f66f16b3 100644 --- a/packages/server/lib/plugins/exchange/binance.js +++ b/packages/server/lib/plugins/exchange/binance.js @@ -10,12 +10,19 @@ const FIAT = ['EUR'] const DEFAULT_FIAT_MARKET = 'EUR' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] -const loadConfig = (account) => { +const loadConfig = account => { const mapper = { - 'privateKey': 'secret' + privateKey: 'secret', } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, +} diff --git a/packages/server/lib/plugins/exchange/binanceus.js b/packages/server/lib/plugins/exchange/binanceus.js index 1c57b239..7699de8e 100644 --- a/packages/server/lib/plugins/exchange/binanceus.js +++ b/packages/server/lib/plugins/exchange/binanceus.js @@ -10,12 +10,19 @@ const FIAT = ['USD'] const DEFAULT_FIAT_MARKET = 'USD' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] -const loadConfig = (account) => { +const loadConfig = account => { const mapper = { - 'privateKey': 'secret' + privateKey: 'secret', } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, +} diff --git a/packages/server/lib/plugins/exchange/bitfinex.js b/packages/server/lib/plugins/exchange/bitfinex.js index a8796f7d..2e1865b7 100644 --- a/packages/server/lib/plugins/exchange/bitfinex.js +++ b/packages/server/lib/plugins/exchange/bitfinex.js @@ -11,12 +11,20 @@ const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 8 const REQUIRED_CONFIG_FIELDS = ['key', 'secret'] -const loadConfig = (account) => { +const loadConfig = account => { const mapper = { - 'key': 'apiKey', + key: 'apiKey', } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { + loadConfig, + REQUIRED_CONFIG_FIELDS, + DEFAULT_FIAT_MARKET, + CRYPTO, + FIAT, + ORDER_TYPE, + AMOUNT_PRECISION, +} diff --git a/packages/server/lib/plugins/exchange/bitstamp.js b/packages/server/lib/plugins/exchange/bitstamp.js index 959e7619..a0399b71 100644 --- a/packages/server/lib/plugins/exchange/bitstamp.js +++ b/packages/server/lib/plugins/exchange/bitstamp.js @@ -1,23 +1,31 @@ -const { COINS } = require('@lamassu/coins') -const _ = require('lodash/fp') - -const { ORDER_TYPES } = require('./consts') - -const ORDER_TYPE = ORDER_TYPES.MARKET -const { BTC, ETH, LTC, BCH, USDT, LN, USDC } = COINS -const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN, USDC] -const FIAT = ['USD', 'EUR'] -const DEFAULT_FIAT_MARKET = 'EUR' -const AMOUNT_PRECISION = 8 -const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket'] - -const loadConfig = (account) => { - const mapper = { - 'key': 'apiKey', - 'clientId': 'uid' - } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) - return { ...mapped, timeout: 3000 } -} - -module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +const { COINS } = require('@lamassu/coins') +const _ = require('lodash/fp') + +const { ORDER_TYPES } = require('./consts') + +const ORDER_TYPE = ORDER_TYPES.MARKET +const { BTC, ETH, LTC, BCH, USDT, LN, USDC } = COINS +const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN, USDC] +const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' +const AMOUNT_PRECISION = 8 +const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket'] + +const loadConfig = account => { + const mapper = { + key: 'apiKey', + clientId: 'uid', + } + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) + return { ...mapped, timeout: 3000 } +} + +module.exports = { + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, + AMOUNT_PRECISION, +} diff --git a/packages/server/lib/plugins/exchange/ccxt.js b/packages/server/lib/plugins/exchange/ccxt.js index a69021ba..ab98e22f 100644 --- a/packages/server/lib/plugins/exchange/ccxt.js +++ b/packages/server/lib/plugins/exchange/ccxt.js @@ -1,92 +1,129 @@ -const { utils: coinUtils } = require('@lamassu/coins') -const _ = require('lodash/fp') -const ccxt = require('ccxt') -const mem = require('mem') - -const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') -const { ORDER_TYPES } = require('./consts') -const logger = require('../../logger') -const { currencies } = require('../../new-admin/config') -const T = require('../../time') - -const DEFAULT_PRICE_PRECISION = 2 -const DEFAULT_AMOUNT_PRECISION = 8 - -function trade (side, account, tradeEntry, exchangeName) { - const { cryptoAtoms, fiatCode, cryptoCode: _cryptoCode, tradeId } = tradeEntry - try { - const cryptoCode = coinUtils.getEquivalentCode(_cryptoCode) - const exchangeConfig = ALL[exchangeName] - if (!exchangeConfig) throw Error('Exchange configuration not found') - - const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig - if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config') - - const selectedFiatMarket = account.currencyMarket - const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName) - const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) - const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) - const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} - const withCustomKey = USER_REF ? { [USER_REF]: tradeId } : {} - const options = _.assign(accountOptions, withCustomKey) - const exchange = new ccxt[exchangeName](loadConfig(account)) - - if (ORDER_TYPE === ORDER_TYPES.MARKET) { - return exchange.createOrder(symbol, ORDER_TYPES.MARKET, side, amount, null, options) - } - - return exchange.fetchOrderBook(symbol) - .then(orderBook => { - const price = calculatePrice(side, amount, orderBook).toFixed(DEFAULT_PRICE_PRECISION) - return exchange.createOrder(symbol, ORDER_TYPES.LIMIT, side, amount, price, options) - }) - } catch (e) { - return Promise.reject(e) - } -} - -function calculatePrice (side, amount, orderBook) { - const book = side === 'buy' ? 'asks' : 'bids' - let collected = 0.0 - for (const entry of orderBook[book]) { - collected += parseFloat(entry[1]) - if (collected >= amount) return parseFloat(entry[0]) - } - throw new Error('Insufficient market depth') -} - -function _getMarkets (exchangeName, availableCryptos) { - const prunedCryptos = _.compose(_.uniq, _.map(coinUtils.getEquivalentCode))(availableCryptos) - - try { - const exchange = new ccxt[exchangeName]() - const cryptosToQuoteAgainst = ['USDT'] - const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst) - - return exchange.fetchMarkets() - .then(_.filter(it => (it.type === 'spot' || it.spot))) - .then(res => - _.reduce((acc, value) => { - if (_.includes(value.base, prunedCryptos) && _.includes(value.quote, currencyCodes)) { - if (value.quote === value.base) return acc - - if (_.isNil(acc[value.quote])) { - return { ...acc, [value.quote]: [value.base] } - } - - acc[value.quote].push(value.base) - } - return acc - }, {}, res) - ) - } catch (e) { - logger.debug(`No CCXT exchange found for ${exchangeName}`) - } -} - -const getMarkets = mem(_getMarkets, { - maxAge: T.week, - cacheKey: (exchangeName, availableCryptos) => exchangeName -}) - -module.exports = { trade, getMarkets } +const { utils: coinUtils } = require('@lamassu/coins') +const _ = require('lodash/fp') +const ccxt = require('ccxt') +const mem = require('mem') + +const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') +const { ORDER_TYPES } = require('./consts') +const logger = require('../../logger') +const { currencies } = require('../../new-admin/config') +const T = require('../../time') + +const DEFAULT_PRICE_PRECISION = 2 +const DEFAULT_AMOUNT_PRECISION = 8 + +function trade(side, account, tradeEntry, exchangeName) { + const { cryptoAtoms, cryptoCode: _cryptoCode, tradeId } = tradeEntry + try { + const cryptoCode = coinUtils.getEquivalentCode(_cryptoCode) + const exchangeConfig = ALL[exchangeName] + if (!exchangeConfig) throw Error('Exchange configuration not found') + + const { + USER_REF, + loadOptions, + loadConfig = _.noop, + REQUIRED_CONFIG_FIELDS, + ORDER_TYPE, + AMOUNT_PRECISION, + } = exchangeConfig + if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) + throw Error('Invalid config') + + const selectedFiatMarket = account.currencyMarket + const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName) + const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) + const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) + const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} + const withCustomKey = USER_REF ? { [USER_REF]: tradeId } : {} + const options = _.assign(accountOptions, withCustomKey) + const exchange = new ccxt[exchangeName](loadConfig(account)) + + if (ORDER_TYPE === ORDER_TYPES.MARKET) { + return exchange.createOrder( + symbol, + ORDER_TYPES.MARKET, + side, + amount, + null, + options, + ) + } + + return exchange.fetchOrderBook(symbol).then(orderBook => { + const price = calculatePrice(side, amount, orderBook).toFixed( + DEFAULT_PRICE_PRECISION, + ) + return exchange.createOrder( + symbol, + ORDER_TYPES.LIMIT, + side, + amount, + price, + options, + ) + }) + } catch (e) { + return Promise.reject(e) + } +} + +function calculatePrice(side, amount, orderBook) { + const book = side === 'buy' ? 'asks' : 'bids' + let collected = 0.0 + for (const entry of orderBook[book]) { + collected += parseFloat(entry[1]) + if (collected >= amount) return parseFloat(entry[0]) + } + throw new Error('Insufficient market depth') +} + +function _getMarkets(exchangeName, availableCryptos) { + const prunedCryptos = _.compose( + _.uniq, + _.map(coinUtils.getEquivalentCode), + )(availableCryptos) + + try { + const exchange = new ccxt[exchangeName]() + const cryptosToQuoteAgainst = ['USDT'] + const currencyCodes = _.concat( + _.map(it => it.code, currencies), + cryptosToQuoteAgainst, + ) + + return exchange + .fetchMarkets() + .then(_.filter(it => it.type === 'spot' || it.spot)) + .then(res => + _.reduce( + (acc, value) => { + if ( + _.includes(value.base, prunedCryptos) && + _.includes(value.quote, currencyCodes) + ) { + if (value.quote === value.base) return acc + + if (_.isNil(acc[value.quote])) { + return { ...acc, [value.quote]: [value.base] } + } + + acc[value.quote].push(value.base) + } + return acc + }, + {}, + res, + ), + ) + } catch (e) { + logger.debug(`No CCXT exchange found for ${exchangeName}. ${e}`) + } +} + +const getMarkets = mem(_getMarkets, { + maxAge: T.week, + cacheKey: exchangeName => exchangeName, +}) + +module.exports = { trade, getMarkets } diff --git a/packages/server/lib/plugins/exchange/cex.js b/packages/server/lib/plugins/exchange/cex.js index b9687e15..18e0b932 100644 --- a/packages/server/lib/plugins/exchange/cex.js +++ b/packages/server/lib/plugins/exchange/cex.js @@ -10,12 +10,19 @@ const FIAT = ['USD', 'EUR'] const DEFAULT_FIAT_MARKET = 'EUR' const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] -const loadConfig = (account) => { +const loadConfig = account => { const mapper = { - 'privateKey': 'secret' + privateKey: 'secret', } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, +} diff --git a/packages/server/lib/plugins/exchange/consts.js b/packages/server/lib/plugins/exchange/consts.js index 690b75c9..5d0e7914 100644 --- a/packages/server/lib/plugins/exchange/consts.js +++ b/packages/server/lib/plugins/exchange/consts.js @@ -1,7 +1,6 @@ - -const ORDER_TYPES = { - MARKET: 'market', - LIMIT: 'limit' -} - -module.exports = { ORDER_TYPES } +const ORDER_TYPES = { + MARKET: 'market', + LIMIT: 'limit', +} + +module.exports = { ORDER_TYPES } diff --git a/packages/server/lib/plugins/exchange/itbit.js b/packages/server/lib/plugins/exchange/itbit.js index d80268e1..2dfbc597 100644 --- a/packages/server/lib/plugins/exchange/itbit.js +++ b/packages/server/lib/plugins/exchange/itbit.js @@ -1,25 +1,42 @@ -const _ = require('lodash/fp') - -const { ORDER_TYPES } = require('./consts') -const { COINS } = require('@lamassu/coins') - -const ORDER_TYPE = ORDER_TYPES.LIMIT -const { BTC, ETH, USDT, LN } = COINS -const CRYPTO = [BTC, ETH, USDT, LN] -const FIAT = ['USD'] -const DEFAULT_FIAT_MARKET = 'USD' -const AMOUNT_PRECISION = 4 -const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket'] - -const loadConfig = (account) => { - const mapper = { - 'clientKey': 'apiKey', - 'clientSecret': 'secret', - 'userId': 'uid' - } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(_.omit(['walletId'], account)) - return { ...mapped, timeout: 3000 } -} -const loadOptions = ({ walletId }) => ({ walletId }) - -module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +const _ = require('lodash/fp') + +const { ORDER_TYPES } = require('./consts') +const { COINS } = require('@lamassu/coins') + +const ORDER_TYPE = ORDER_TYPES.LIMIT +const { BTC, ETH, USDT, LN } = COINS +const CRYPTO = [BTC, ETH, USDT, LN] +const FIAT = ['USD'] +const DEFAULT_FIAT_MARKET = 'USD' +const AMOUNT_PRECISION = 4 +const REQUIRED_CONFIG_FIELDS = [ + 'clientKey', + 'clientSecret', + 'userId', + 'walletId', + 'currencyMarket', +] + +const loadConfig = account => { + const mapper = { + clientKey: 'apiKey', + clientSecret: 'secret', + userId: 'uid', + } + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))( + _.omit(['walletId'], account), + ) + return { ...mapped, timeout: 3000 } +} +const loadOptions = ({ walletId }) => ({ walletId }) + +module.exports = { + loadOptions, + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, + AMOUNT_PRECISION, +} diff --git a/packages/server/lib/plugins/exchange/kraken.js b/packages/server/lib/plugins/exchange/kraken.js index 29e80630..30d49c40 100644 --- a/packages/server/lib/plugins/exchange/kraken.js +++ b/packages/server/lib/plugins/exchange/kraken.js @@ -1,30 +1,56 @@ -const _ = require('lodash/fp') - -const { ORDER_TYPES } = require('./consts') -const { COINS } = require('@lamassu/coins') - -const ORDER_TYPE = ORDER_TYPES.MARKET -const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN, USDC } = COINS -const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN, USDC] -const FIAT = ['USD', 'EUR'] -const DEFAULT_FIAT_MARKET = 'EUR' -const AMOUNT_PRECISION = 6 -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] -const USER_REF = 'userref' - -const loadConfig = (account) => { - const mapper = { - 'privateKey': 'secret' - } - const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(account) - - return { - ...mapped, - timeout: 3000, - nonce: function () { return this.microseconds() } - } -} - -const loadOptions = () => ({ expiretm: '+60' }) - -module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +const _ = require('lodash/fp') + +const { ORDER_TYPES } = require('./consts') +const { COINS } = require('@lamassu/coins') + +const ORDER_TYPE = ORDER_TYPES.MARKET +const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN, USDC } = + COINS +const CRYPTO = [ + BTC, + ETH, + LTC, + DASH, + ZEC, + BCH, + XMR, + USDT, + TRX, + USDT_TRON, + LN, + USDC, +] +const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' +const AMOUNT_PRECISION = 6 +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] +const USER_REF = 'userref' + +const loadConfig = account => { + const mapper = { + privateKey: 'secret', + } + const mapped = _.mapKeys(key => (mapper[key] ? mapper[key] : key))(account) + + return { + ...mapped, + timeout: 3000, + nonce: function () { + return this.microseconds() + }, + } +} + +const loadOptions = () => ({ expiretm: '+60' }) + +module.exports = { + USER_REF, + loadOptions, + loadConfig, + DEFAULT_FIAT_MARKET, + REQUIRED_CONFIG_FIELDS, + CRYPTO, + FIAT, + ORDER_TYPE, + AMOUNT_PRECISION, +} diff --git a/packages/server/lib/plugins/exchange/mock-exchange.js b/packages/server/lib/plugins/exchange/mock-exchange.js index bdf287da..aaf8ee45 100644 --- a/packages/server/lib/plugins/exchange/mock-exchange.js +++ b/packages/server/lib/plugins/exchange/mock-exchange.js @@ -1,14 +1,24 @@ module.exports = { buy, - sell + sell, } -function buy (cryptoAtoms, fiatCode, cryptoCode) { - console.log('[mock] buying %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode) +function buy(cryptoAtoms, fiatCode, cryptoCode) { + console.log( + '[mock] buying %s %s for %s', + cryptoAtoms.toString(), + cryptoCode, + fiatCode, + ) return Promise.resolve() } -function sell (cryptoAtoms, fiatCode, cryptoCode) { - console.log('[mock] selling %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode) +function sell(cryptoAtoms, fiatCode, cryptoCode) { + console.log( + '[mock] selling %s %s for %s', + cryptoAtoms.toString(), + cryptoCode, + fiatCode, + ) return Promise.resolve() } diff --git a/packages/server/lib/plugins/sms/inforu/inforu.js b/packages/server/lib/plugins/sms/inforu/inforu.js index 1453ca7f..754ada2a 100644 --- a/packages/server/lib/plugins/sms/inforu/inforu.js +++ b/packages/server/lib/plugins/sms/inforu/inforu.js @@ -1,46 +1,47 @@ -const axios = require('axios') - -const NAME = 'InforU' - -function sendMessage (account, rec) { - const username = account.username - const apiKey = account.apiKey - - const to = rec.sms.toNumber || account.toNumber - const text = rec.sms.body - const from = account.fromNumber - - const url = 'https://capi.inforu.co.il/api/v2/SMS/SendSms' - - const config = { - auth: { - username: username, - password: apiKey - }, - maxBodyLength: Infinity, - headers:{ - 'Content-Type': 'application/json' - } - } - - const data = { - Message: text, - Recipients: [{ - Phone: to - }], - Settings: { - Sender: from - } - } - - axios.post(url, data, config) - .catch(err => { - // console.log(err) - throw new Error(`inforu error: ${err.message}`) - }) -} - -module.exports = { - NAME, - sendMessage -} +const axios = require('axios') + +const NAME = 'InforU' + +function sendMessage(account, rec) { + const username = account.username + const apiKey = account.apiKey + + const to = rec.sms.toNumber || account.toNumber + const text = rec.sms.body + const from = account.fromNumber + + const url = 'https://capi.inforu.co.il/api/v2/SMS/SendSms' + + const config = { + auth: { + username: username, + password: apiKey, + }, + maxBodyLength: Infinity, + headers: { + 'Content-Type': 'application/json', + }, + } + + const data = { + Message: text, + Recipients: [ + { + Phone: to, + }, + ], + Settings: { + Sender: from, + }, + } + + axios.post(url, data, config).catch(err => { + // console.log(err) + throw new Error(`inforu error: ${err.message}`) + }) +} + +module.exports = { + NAME, + sendMessage, +} diff --git a/packages/server/lib/plugins/sms/mock-sms/mock-sms.js b/packages/server/lib/plugins/sms/mock-sms/mock-sms.js index 1bd021b3..fd631200 100644 --- a/packages/server/lib/plugins/sms/mock-sms/mock-sms.js +++ b/packages/server/lib/plugins/sms/mock-sms/mock-sms.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') const NAME = 'MockSMS' -function sendMessage (account, rec) { +function sendMessage(account, rec) { console.log('Sending SMS: %j', rec) return new Promise((resolve, reject) => { if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) { @@ -15,5 +15,5 @@ function sendMessage (account, rec) { module.exports = { NAME, - sendMessage + sendMessage, } diff --git a/packages/server/lib/plugins/sms/telnyx/telnyx.js b/packages/server/lib/plugins/sms/telnyx/telnyx.js index 74c3bcd0..e8d57428 100644 --- a/packages/server/lib/plugins/sms/telnyx/telnyx.js +++ b/packages/server/lib/plugins/sms/telnyx/telnyx.js @@ -2,20 +2,19 @@ const Telnyx = require('telnyx') const NAME = 'Telnyx' -function sendMessage (account, rec) { +function sendMessage(account, rec) { const telnyx = Telnyx(account.apiKey) const from = account.fromNumber const text = rec.sms.body const to = rec.sms.toNumber || account.toNumber - return telnyx.messages.create({ from, to, text }) - .catch(err => { - throw new Error(`Telnyx error: ${err.message}`) - }) + return telnyx.messages.create({ from, to, text }).catch(err => { + throw new Error(`Telnyx error: ${err.message}`) + }) } module.exports = { NAME, - sendMessage + sendMessage, } diff --git a/packages/server/lib/plugins/sms/twilio/twilio.js b/packages/server/lib/plugins/sms/twilio/twilio.js index cedd6952..1ba462a6 100644 --- a/packages/server/lib/plugins/sms/twilio/twilio.js +++ b/packages/server/lib/plugins/sms/twilio/twilio.js @@ -3,10 +3,12 @@ const _ = require('lodash/fp') const NAME = 'Twilio' -const BAD_NUMBER_CODES = [21201, 21202, 21211, 21214, 21216, 21217, 21219, 21408, - 21610, 21612, 21614, 21608] +const BAD_NUMBER_CODES = [ + 21201, 21202, 21211, 21214, 21216, 21217, 21219, 21408, 21610, 21612, 21614, + 21608, +] -function sendMessage (account, rec) { +function sendMessage(account, rec) { return Promise.resolve() .then(() => { // to catch configuration errors like @@ -14,14 +16,16 @@ function sendMessage (account, rec) { const client = twilio(account.accountSid, account.authToken) const body = rec.sms.body const _toNumber = rec.sms.toNumber || account.toNumber - const from = (_.startsWith('+')(account.fromNumber) - || !_.isNumber(String(account.fromNumber).replace(/\s/g,''))) - ? account.fromNumber : `+${account.fromNumber}` + const from = + _.startsWith('+')(account.fromNumber) || + !_.isNumber(String(account.fromNumber).replace(/\s/g, '')) + ? account.fromNumber + : `+${account.fromNumber}` const opts = { body: body, to: _toNumber, - from + from, } return client.messages.create(opts) @@ -39,5 +43,5 @@ function sendMessage (account, rec) { module.exports = { NAME, - sendMessage + sendMessage, } diff --git a/packages/server/lib/plugins/sms/vonage/vonage.js b/packages/server/lib/plugins/sms/vonage/vonage.js index 2582f695..510ae716 100644 --- a/packages/server/lib/plugins/sms/vonage/vonage.js +++ b/packages/server/lib/plugins/sms/vonage/vonage.js @@ -3,10 +3,10 @@ const { SMS } = require('@vonage/sms') const NAME = 'Vonage' -function sendMessage (account, rec) { +function sendMessage(account, rec) { const credentials = new Auth({ apiKey: account.apiKey, - apiSecret: account.apiSecret + apiSecret: account.apiSecret, }) const from = account.fromNumber @@ -14,10 +14,9 @@ function sendMessage (account, rec) { const to = rec.sms.toNumber || account.toNumber const smsClient = new SMS(credentials) - smsClient.send({ from, text, to }) - .catch(err => { - throw new Error(`Vonage error: ${err.message}`) - }) + smsClient.send({ from, text, to }).catch(err => { + throw new Error(`Vonage error: ${err.message}`) + }) } module.exports = { diff --git a/packages/server/lib/plugins/sms/whatsapp/whatsapp.js b/packages/server/lib/plugins/sms/whatsapp/whatsapp.js index c2454665..2ded3d3f 100644 --- a/packages/server/lib/plugins/sms/whatsapp/whatsapp.js +++ b/packages/server/lib/plugins/sms/whatsapp/whatsapp.js @@ -1,42 +1,41 @@ -const axios = require('axios') - -const NAME = 'Whatsapp' - -function sendMessage (account, rec) { - const phoneId = account.phoneId - const token = account.apiKey - - const to = rec.sms.toNumber || account.toNumber - const template = rec.sms.template - - const url = `https://graph.facebook.com/v17.0/${phoneId}/messages` - - const config = { - headers:{ - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json' - } - } - - const data = { - messaging_product: 'whatsapp', - recipient_type: 'individual', - type: 'template', - to, - template: { - name: template, - language: { code: 'en_US' } - } - } - - axios.post(url, data, config) - .catch(err => { - // console.log(err) - throw new Error(`Whatsapp error: ${err.message}`) - }) -} - -module.exports = { - NAME, - sendMessage -} +const axios = require('axios') + +const NAME = 'Whatsapp' + +function sendMessage(account, rec) { + const phoneId = account.phoneId + const token = account.apiKey + + const to = rec.sms.toNumber || account.toNumber + const template = rec.sms.template + + const url = `https://graph.facebook.com/v17.0/${phoneId}/messages` + + const config = { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + + const data = { + messaging_product: 'whatsapp', + recipient_type: 'individual', + type: 'template', + to, + template: { + name: template, + language: { code: 'en_US' }, + }, + } + + axios.post(url, data, config).catch(err => { + // console.log(err) + throw new Error(`Whatsapp error: ${err.message}`) + }) +} + +module.exports = { + NAME, + sendMessage, +} diff --git a/packages/server/lib/plugins/ticker/bitpay.js b/packages/server/lib/plugins/ticker/bitpay.js index 0952ab40..dbfe6fd2 100644 --- a/packages/server/lib/plugins/ticker/bitpay.js +++ b/packages/server/lib/plugins/ticker/bitpay.js @@ -7,16 +7,17 @@ const { BTC, BCH, LN } = COINS const CRYPTO = [BTC, BCH, LN] const FIAT = 'ALL_CURRENCIES' -function ticker (fiatCode, cryptoCode) { - return axios.get('https://bitpay.com/rates/' + cryptoCode + '/' + fiatCode) +function ticker(fiatCode, cryptoCode) { + return axios + .get('https://bitpay.com/rates/' + cryptoCode + '/' + fiatCode) .then(r => { const data = r.data.data const price = new BN(data.rate.toString()) return { rates: { ask: price, - bid: price - } + bid: price, + }, } }) } @@ -25,5 +26,5 @@ module.exports = { ticker, name: 'BitPay', CRYPTO, - FIAT + FIAT, } diff --git a/packages/server/lib/plugins/ticker/ccxt.js b/packages/server/lib/plugins/ticker/ccxt.js index 881085d1..7316d61d 100644 --- a/packages/server/lib/plugins/ticker/ccxt.js +++ b/packages/server/lib/plugins/ticker/ccxt.js @@ -1,7 +1,11 @@ const ccxt = require('ccxt') const BN = require('../../bn') -const { buildMarket, verifyFiatSupport, defaultFiatMarket } = require('../common/ccxt') +const { + buildMarket, + verifyFiatSupport, + defaultFiatMarket, +} = require('../common/ccxt') const { getRate } = require('../../../lib/forex') const RETRIES = 2 @@ -16,7 +20,7 @@ const sanityCheckRates = (ask, bid, tickerName) => { } } -function ticker (fiatCode, cryptoCode, tickerName) { +function ticker(fiatCode, cryptoCode, tickerName) { if (!tickerObjects[tickerName]) { tickerObjects[tickerName] = new ccxt[tickerName]({ timeout: 3000, @@ -30,37 +34,40 @@ function ticker (fiatCode, cryptoCode, tickerName) { return getCurrencyRates(ticker, fiatCode, cryptoCode) } - return getRate(RETRIES, tickerName, defaultFiatMarket(tickerName)) - .then(({ fxRate }) => { + return getRate(RETRIES, tickerName, defaultFiatMarket(tickerName)).then( + ({ fxRate }) => { try { - return getCurrencyRates(ticker, defaultFiatMarket(tickerName), cryptoCode) - .then(res => ({ - rates: { - ask: res.rates.ask.times(fxRate), - bid: res.rates.bid.times(fxRate) - } - })) + return getCurrencyRates( + ticker, + defaultFiatMarket(tickerName), + 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) { +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 => { - sanityCheckRates(res.ask, res.bid, cryptoCode) - return { - rates: { - ask: new BN(res.ask), - bid: new BN(res.bid) - } - } + return ticker.fetchTicker(symbol).then(res => { + sanityCheckRates(res.ask, res.bid, cryptoCode) + return { + rates: { + ask: new BN(res.ask), + bid: new BN(res.bid), + }, + } }) } catch (e) { return Promise.reject(e) diff --git a/packages/server/lib/plugins/ticker/mock-ticker.js b/packages/server/lib/plugins/ticker/mock-ticker.js index 6070e0c0..df038519 100644 --- a/packages/server/lib/plugins/ticker/mock-ticker.js +++ b/packages/server/lib/plugins/ticker/mock-ticker.js @@ -1,12 +1,12 @@ const BN = require('../../bn') -function ticker (fiatCode, cryptoCode) { +function ticker() { return Promise.resolve({ rates: { ask: new BN(105), - bid: new BN(100) - } + bid: new BN(100), + }, }) } -module.exports = {ticker} +module.exports = { ticker } diff --git a/packages/server/lib/plugins/ticker/pazuz-ticker/pazuz-ticker.js b/packages/server/lib/plugins/ticker/pazuz-ticker/pazuz-ticker.js index 34daf691..691b19ec 100644 --- a/packages/server/lib/plugins/ticker/pazuz-ticker/pazuz-ticker.js +++ b/packages/server/lib/plugins/ticker/pazuz-ticker/pazuz-ticker.js @@ -3,26 +3,25 @@ const axios = require('axios').create({ // TODO: get rejectUnauthorized true to work baseURL: `${process.env.TICKER_URL}/api/rates/`, httpsAgent: new https.Agent({ - rejectUnauthorized: false - }) + rejectUnauthorized: false, + }), }) const BN = require('../../../bn') -function ticker (account, fiatCode, cryptoCode) { - return axios.get(`${cryptoCode}/${fiatCode}`) - .then(({ data }) => { - if (data.error) throw new Error(JSON.stringify(data.error)) - return { - rates: { - ask: BN(data.ask), - bid: BN(data.bid), - signature: data.signature - } - } - }) +function ticker(account, fiatCode, cryptoCode) { + return axios.get(`${cryptoCode}/${fiatCode}`).then(({ data }) => { + if (data.error) throw new Error(JSON.stringify(data.error)) + return { + rates: { + ask: BN(data.ask), + bid: BN(data.bid), + signature: data.signature, + }, + } + }) } module.exports = { - ticker + ticker, } diff --git a/packages/server/lib/plugins/tokens/erc20.abi.json b/packages/server/lib/plugins/tokens/erc20.abi.json index b66cb33f..db288ccd 100644 --- a/packages/server/lib/plugins/tokens/erc20.abi.json +++ b/packages/server/lib/plugins/tokens/erc20.abi.json @@ -1,282 +1,272 @@ [ { - "constant":true, - "inputs":[ - - ], - "name":"name", - "outputs":[ + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ { - "name":"", - "type":"string" + "name": "", + "type": "string" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":false, - "inputs":[ + "constant": false, + "inputs": [ { - "name":"_spender", - "type":"address" + "name": "_spender", + "type": "address" }, { - "name":"_value", - "type":"uint256" + "name": "_value", + "type": "uint256" } ], - "name":"approve", - "outputs":[ + "name": "approve", + "outputs": [ { - "name":"success", - "type":"bool" + "name": "success", + "type": "bool" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ - - ], - "name":"totalSupply", - "outputs":[ + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ { - "name":"", - "type":"uint256" + "name": "", + "type": "uint256" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":false, - "inputs":[ + "constant": false, + "inputs": [ { - "name":"_from", - "type":"address" + "name": "_from", + "type": "address" }, { - "name":"_to", - "type":"address" + "name": "_to", + "type": "address" }, { - "name":"_value", - "type":"uint256" + "name": "_value", + "type": "uint256" } ], - "name":"transferFrom", - "outputs":[ + "name": "transferFrom", + "outputs": [ { - "name":"success", - "type":"bool" + "name": "success", + "type": "bool" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ - - ], - "name":"decimals", - "outputs":[ + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ { - "name":"", - "type":"uint256" + "name": "", + "type": "uint256" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ - - ], - "name":"version", - "outputs":[ + "constant": true, + "inputs": [], + "name": "version", + "outputs": [ { - "name":"", - "type":"string" + "name": "", + "type": "string" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ + "constant": true, + "inputs": [ { - "name":"_owner", - "type":"address" + "name": "_owner", + "type": "address" } ], - "name":"balanceOf", - "outputs":[ + "name": "balanceOf", + "outputs": [ { - "name":"balance", - "type":"uint256" + "name": "balance", + "type": "uint256" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ - - ], - "name":"symbol", - "outputs":[ + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ { - "name":"", - "type":"string" + "name": "", + "type": "string" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":false, - "inputs":[ + "constant": false, + "inputs": [ { - "name":"_to", - "type":"address" + "name": "_to", + "type": "address" }, { - "name":"_value", - "type":"uint256" + "name": "_value", + "type": "uint256" } ], - "name":"transfer", - "outputs":[ + "name": "transfer", + "outputs": [ { - "name":"success", - "type":"bool" + "name": "success", + "type": "bool" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":false, - "inputs":[ + "constant": false, + "inputs": [ { - "name":"_spender", - "type":"address" + "name": "_spender", + "type": "address" }, { - "name":"_value", - "type":"uint256" + "name": "_value", + "type": "uint256" }, { - "name":"_extraData", - "type":"bytes" + "name": "_extraData", + "type": "bytes" } ], - "name":"approveAndCall", - "outputs":[ + "name": "approveAndCall", + "outputs": [ { - "name":"success", - "type":"bool" + "name": "success", + "type": "bool" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "constant":true, - "inputs":[ + "constant": true, + "inputs": [ { - "name":"_owner", - "type":"address" + "name": "_owner", + "type": "address" }, { - "name":"_spender", - "type":"address" + "name": "_spender", + "type": "address" } ], - "name":"allowance", - "outputs":[ + "name": "allowance", + "outputs": [ { - "name":"remaining", - "type":"uint256" + "name": "remaining", + "type": "uint256" } ], - "payable":false, - "type":"function" + "payable": false, + "type": "function" }, { - "inputs":[ + "inputs": [ { - "name":"_initialAmount", - "type":"uint256" + "name": "_initialAmount", + "type": "uint256" }, { - "name":"_tokenName", - "type":"string" + "name": "_tokenName", + "type": "string" }, { - "name":"_decimalUnits", - "type":"uint8" + "name": "_decimalUnits", + "type": "uint8" }, { - "name":"_tokenSymbol", - "type":"string" + "name": "_tokenSymbol", + "type": "string" } ], - "type":"constructor" + "type": "constructor" }, { - "payable":false, - "type":"fallback" + "payable": false, + "type": "fallback" }, { - "anonymous":false, - "inputs":[ + "anonymous": false, + "inputs": [ { - "indexed":true, - "name":"_from", - "type":"address" + "indexed": true, + "name": "_from", + "type": "address" }, { - "indexed":true, - "name":"_to", - "type":"address" + "indexed": true, + "name": "_to", + "type": "address" }, { - "indexed":false, - "name":"_value", - "type":"uint256" + "indexed": false, + "name": "_value", + "type": "uint256" } ], - "name":"Transfer", - "type":"event" + "name": "Transfer", + "type": "event" }, { - "anonymous":false, - "inputs":[ + "anonymous": false, + "inputs": [ { - "indexed":true, - "name":"_owner", - "type":"address" + "indexed": true, + "name": "_owner", + "type": "address" }, { - "indexed":true, - "name":"_spender", - "type":"address" + "indexed": true, + "name": "_spender", + "type": "address" }, { - "indexed":false, - "name":"_value", - "type":"uint256" + "indexed": false, + "name": "_value", + "type": "uint256" } ], - "name":"Approval", - "type":"event" + "name": "Approval", + "type": "event" } -] \ No newline at end of file +] diff --git a/packages/server/lib/plugins/wallet-scoring/elliptic/elliptic.js b/packages/server/lib/plugins/wallet-scoring/elliptic/elliptic.js index ca17c11e..d9942112 100644 --- a/packages/server/lib/plugins/wallet-scoring/elliptic/elliptic.js +++ b/packages/server/lib/plugins/wallet-scoring/elliptic/elliptic.js @@ -9,34 +9,34 @@ const HOLLISTIC_COINS = { USDT: 'USDT', USDT_TRON: 'USDT', LTC: 'LTC', - TRX: 'TRX' + TRX: 'TRX', } const SINGLE_ASSET_COINS = { ZEC: { asset: 'ZEC', - blockchain: 'zcash' + blockchain: 'zcash', }, BCH: { asset: 'BCH', - blockchain: 'bitcoin_cash' - } + blockchain: 'bitcoin_cash', + }, } const TYPE = { - TRANSACTION: 'transaction', - ADDRESS: 'address' + TRANSACTION: 'transaction', + ADDRESS: 'address', } const SUPPORTED_COINS = { ...HOLLISTIC_COINS, ...SINGLE_ASSET_COINS } -function rate (account, objectType, cryptoCode, objectId) { +function rate(account, objectType, cryptoCode, objectId) { return isWalletScoringEnabled(account, cryptoCode).then(isEnabled => { if (!isEnabled) return Promise.resolve(null) const aml = new AML({ key: account.apiKey, - secret: account.apiSecret + secret: account.apiSecret, }) const isHolistic = Object.keys(HOLLISTIC_COINS).includes(cryptoCode) @@ -44,38 +44,44 @@ function rate (account, objectType, cryptoCode, objectId) { const requestBody = { subject: { asset: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].asset, - blockchain: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].blockchain, + blockchain: isHolistic + ? 'holistic' + : SINGLE_ASSET_COINS[cryptoCode].blockchain, type: objectType, - hash: objectId + hash: objectId, }, - type: objectType === TYPE.ADDRESS ? 'wallet_exposure' : 'source_of_funds' + type: objectType === TYPE.ADDRESS ? 'wallet_exposure' : 'source_of_funds', } const threshold = account.scoreThreshold - const endpoint = objectType === TYPE.ADDRESS ? '/v2/wallet/synchronous' : '/v2/analysis/synchronous' + const endpoint = + objectType === TYPE.ADDRESS + ? '/v2/wallet/synchronous' + : '/v2/analysis/synchronous' - return aml.client - .post(endpoint, requestBody) - .then((res) => { - const resScore = res.data?.risk_score + return aml.client.post(endpoint, requestBody).then(res => { + const resScore = res.data?.risk_score - // elliptic returns 0-1 score, but we're accepting 0-100 config - // normalize score to 0-10 where 0 is the lowest risk - // elliptic score can be null and contains decimals - return {score: (resScore || 0) * 10, isValid: ((resScore || 0) * 100) < threshold} - }) + // elliptic returns 0-1 score, but we're accepting 0-100 config + // normalize score to 0-10 where 0 is the lowest risk + // elliptic score can be null and contains decimals + return { + score: (resScore || 0) * 10, + isValid: (resScore || 0) * 100 < threshold, + } + }) }) } -function rateTransaction (account, cryptoCode, transactionId) { +function rateTransaction(account, cryptoCode, transactionId) { return rate(account, TYPE.TRANSACTION, cryptoCode, transactionId) } -function rateAddress (account, cryptoCode, address) { +function rateAddress(account, cryptoCode, address) { return rate(account, TYPE.ADDRESS, cryptoCode, address) } -function isWalletScoringEnabled (account, cryptoCode) { +function isWalletScoringEnabled(account, cryptoCode) { const isAccountEnabled = !_.isNil(account) && account.enabled if (!isAccountEnabled) return Promise.resolve(false) @@ -91,5 +97,5 @@ module.exports = { NAME, rateAddress, rateTransaction, - isWalletScoringEnabled + isWalletScoringEnabled, } diff --git a/packages/server/lib/plugins/wallet-scoring/mock-scoring/mock-scoring.js b/packages/server/lib/plugins/wallet-scoring/mock-scoring/mock-scoring.js index 0f154ffc..44e00d0a 100644 --- a/packages/server/lib/plugins/wallet-scoring/mock-scoring/mock-scoring.js +++ b/packages/server/lib/plugins/wallet-scoring/mock-scoring/mock-scoring.js @@ -2,18 +2,22 @@ const NAME = 'FakeScoring' const { WALLET_SCORE_THRESHOLD } = require('../../../constants') -function rateAddress (account, cryptoCode, address) { - return new Promise((resolve, _) => { +function rateAddress(account, cryptoCode, address) { + return new Promise(resolve => { setTimeout(() => { - console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address) - return Promise.resolve(2) - .then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD })) + console.log( + '[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', + address, + ) + return Promise.resolve(2).then(score => + resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD }), + ) }, 100) }) } -function isWalletScoringEnabled (account, cryptoCode) { - return new Promise((resolve, _) => { +function isWalletScoringEnabled() { + return new Promise(resolve => { setTimeout(() => { return resolve(true) }, 100) @@ -23,6 +27,6 @@ function isWalletScoringEnabled (account, cryptoCode) { module.exports = { NAME, rateAddress, - rateTransaction:rateAddress, - isWalletScoringEnabled + rateTransaction: rateAddress, + isWalletScoringEnabled, } diff --git a/packages/server/lib/plugins/wallet-scoring/scorechain/scorechain.js b/packages/server/lib/plugins/wallet-scoring/scorechain/scorechain.js index 3a64d4b8..6d7028b4 100644 --- a/packages/server/lib/plugins/wallet-scoring/scorechain/scorechain.js +++ b/packages/server/lib/plugins/wallet-scoring/scorechain/scorechain.js @@ -1,7 +1,6 @@ const axios = require('axios') const _ = require('lodash/fp') - const NAME = 'Scorechain' const SUPPORTED_COINS = { BTC: 'BITCOIN', @@ -11,15 +10,15 @@ const SUPPORTED_COINS = { LTC: 'LITECOIN', DASH: 'DASH', TRX: 'TRON', - USDT_TRON: 'TRON' + USDT_TRON: 'TRON', } const TYPE = { TRANSACTION: 'TRANSACTION', - ADDRESS: 'ADDRESS' + ADDRESS: 'ADDRESS', } -function rate (account, objectType, cryptoCode, objectId) { +function rate(account, objectType, cryptoCode, objectId) { return isWalletScoringEnabled(account, cryptoCode).then(isEnabled => { if (!isEnabled) return Promise.resolve(null) @@ -29,21 +28,25 @@ function rate (account, objectType, cryptoCode, objectId) { objectType, objectId, blockchain: SUPPORTED_COINS[cryptoCode], - coin: "ALL" + coin: 'ALL', } const headers = { - 'accept': 'application/json', + accept: 'application/json', 'X-API-KEY': account.apiKey, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', } - return axios.post(`https://api.scorechain.com/v1/scoringAnalysis`, payload, {headers}) + return axios + .post(`https://api.scorechain.com/v1/scoringAnalysis`, payload, { + headers, + }) .then(res => { const resScore = res.data?.analysis?.assigned?.result?.score - if (!resScore) throw new Error('Failed to get score from Scorechain API') + if (!resScore) + throw new Error('Failed to get score from Scorechain API') // normalize score to 0-10 where 0 is the lowest risk - return {score: (100 - resScore) / 10, isValid: resScore >= threshold} + return { score: (100 - resScore) / 10, isValid: resScore >= threshold } }) .catch(err => { throw err @@ -51,15 +54,15 @@ function rate (account, objectType, cryptoCode, objectId) { }) } -function rateTransaction (account, cryptoCode, transactionId) { +function rateTransaction(account, cryptoCode, transactionId) { return rate(account, TYPE.TRANSACTION, cryptoCode, transactionId) } -function rateAddress (account, cryptoCode, address) { +function rateAddress(account, cryptoCode, address) { return rate(account, TYPE.ADDRESS, cryptoCode, address) } -function isWalletScoringEnabled (account, cryptoCode) { +function isWalletScoringEnabled(account, cryptoCode) { const isAccountEnabled = !_.isNil(account) && account.enabled if (!isAccountEnabled) return Promise.resolve(false) @@ -75,5 +78,5 @@ module.exports = { NAME, rateAddress, rateTransaction, - isWalletScoringEnabled + isWalletScoringEnabled, } diff --git a/packages/server/lib/plugins/wallet/bitcoincashd/bitcoincashd.js b/packages/server/lib/plugins/wallet/bitcoincashd/bitcoincashd.js index 1208a19e..4ddbc161 100644 --- a/packages/server/lib/plugins/wallet/bitcoincashd/bitcoincashd.js +++ b/packages/server/lib/plugins/wallet/bitcoincashd/bitcoincashd.js @@ -10,11 +10,11 @@ const unitScale = cryptoRec.unitScale const rpcConfig = jsonRpc.rpcConfig(cryptoRec) -function fetch (method, params) { +function fetch(method, params) { return jsonRpc.fetch(rpcConfig, method, params) } -function errorHandle (e) { +function errorHandle(e) { const err = JSON.parse(e.message) switch (err.code) { case -6: @@ -24,115 +24,130 @@ function errorHandle (e) { } } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'BCH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'BCH') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } -function accountUnconfirmedBalance (cryptoCode) { +function accountUnconfirmedBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ unconfirmed_balance: balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } // We want a balance that includes all spends (0 conf) but only deposits that // have at least 1 confirmation. getbalance does this for us automatically. -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) return checkCryptoCode(cryptoCode) .then(() => fetch('sendtoaddress', [toAddress, coins])) - .then((txId) => fetch('gettransaction', [txId])) - .then((res) => _.pick(['fee', 'txid'], res)) - .then((pickedObj) => { + .then(txId => fetch('gettransaction', [txId])) + .then(res => _.pick(['fee', 'txid'], res)) + .then(pickedObj => { return { fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, } }) .catch(errorHandle) } -function newAddress (account, info, tx, settings, operatorId) { - return checkCryptoCode(info.cryptoCode) - .then(() => fetch('getnewaddress')) +function newAddress(account, info) { + return checkCryptoCode(info.cryptoCode).then(() => fetch('getnewaddress')) } -function addressBalance (address, confs) { - return fetch('getreceivedbyaddress', [address, confs]) - .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0)) +function addressBalance(address, confs) { + return fetch('getreceivedbyaddress', [address, confs]).then(r => + new BN(r).shiftedBy(unitScale).decimalPlaces(0), + ) } -function confirmedBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 1)) +function confirmedBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 1)) } -function pendingBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 0)) +function pendingBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 0)) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => confirmedBalance(toAddress, cryptoCode)) .then(confirmed => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, cryptoCode) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, cryptoCode).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => { const promises = [ accountUnconfirmedBalance(cryptoCode), accountBalance(cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] return Promise.all(promises) }) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) + .then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) } -function cryptoNetwork (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main') +function cryptoNetwork(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => + parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main', + ) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getblockchaininfo')) - .then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready') + .then(res => (res['initialblockdownload'] ? 'syncing' : 'ready')) } -function getTxHashesByAddress (cryptoCode, address) { +function getTxHashesByAddress(cryptoCode, address) { checkCryptoCode(cryptoCode) .then(() => fetch('listreceivedbyaddress', [0, true, true, address])) - .then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress)))) + .then(txsByAddress => + Promise.all( + _.map( + id => fetch('getrawtransaction', [id]), + _.flatMap(it => it.txids, txsByAddress), + ), + ), + ) .then(_.map(({ hash }) => hash)) } @@ -144,5 +159,5 @@ module.exports = { newFunding, cryptoNetwork, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/wallet/bitcoind/bitcoind.js b/packages/server/lib/plugins/wallet/bitcoind/bitcoind.js index ba5d17ae..12bd090f 100644 --- a/packages/server/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/packages/server/lib/plugins/wallet/bitcoind/bitcoind.js @@ -6,7 +6,6 @@ const BN = require('../../../bn') const E = require('../../../error') const logger = require('../../../logger') const { utils: coinUtils } = require('@lamassu/coins') -const { isDevMode } = require('../../../environment-helper') const cryptoRec = coinUtils.getCryptoCurrency('BTC') const unitScale = cryptoRec.unitScale @@ -15,11 +14,11 @@ const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const SUPPORTS_BATCHING = true -function fetch (method, params) { +function fetch(method, params) { return jsonRpc.fetch(rpcConfig, method, params) } -function errorHandle (e) { +function errorHandle(e) { const err = JSON.parse(e.message) switch (err.code) { case -5: @@ -31,32 +30,40 @@ function errorHandle (e) { } } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'BTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'BTC') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getbalances')) - .then(({ mine }) => new BN(mine.trusted).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ mine }) => + new BN(mine.trusted).shiftedBy(unitScale).decimalPlaces(0), + ) .catch(errorHandle) } -function accountUnconfirmedBalance (cryptoCode) { +function accountUnconfirmedBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getbalances')) - .then(({ mine }) => new BN(mine.untrusted_pending).plus(mine.immature).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ mine }) => + new BN(mine.untrusted_pending) + .plus(mine.immature) + .shiftedBy(unitScale) + .decimalPlaces(0), + ) .catch(errorHandle) } // We want a balance that includes all spends (0 conf) but only deposits that // have at least 1 confirmation. getbalance does this for us automatically. -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function estimateFee () { +function estimateFee() { return getSatBEstimateFee() .then(result => BN(result)) .catch(err => { @@ -64,26 +71,29 @@ function estimateFee () { }) } -function calculateFeeDiscount (feeMultiplier = 1, unitScale) { +function calculateFeeDiscount(feeMultiplier = 1, unitScale) { // 0 makes bitcoind do automatic fee selection const AUTOMATIC_FEE = 0 - return estimateFee() - .then(estimatedFee => { - if (!estimatedFee) { - logger.info('failure estimating fee, using bitcoind automatic fee selection') - return AUTOMATIC_FEE - } - // transform from sat/vB to BTC/kvB and apply the multipler - const newFee = estimatedFee.shiftedBy(-unitScale+3).times(feeMultiplier) - if (newFee.lt(0.00001) || newFee.gt(0.1)) { - logger.info('fee outside safety parameters, defaulting to automatic fee selection') - return AUTOMATIC_FEE - } - return newFee.toFixed(8) - }) + return estimateFee().then(estimatedFee => { + if (!estimatedFee) { + logger.info( + 'failure estimating fee, using bitcoind automatic fee selection', + ) + return AUTOMATIC_FEE + } + // transform from sat/vB to BTC/kvB and apply the multipler + const newFee = estimatedFee.shiftedBy(-unitScale + 3).times(feeMultiplier) + if (newFee.lt(0.00001) || newFee.gt(0.1)) { + logger.info( + 'fee outside safety parameters, defaulting to automatic fee selection', + ) + return AUTOMATIC_FEE + } + return newFee.toFixed(8) + }) } -function sendCoins (account, tx, settings, operatorId, feeMultiplier) { +function sendCoins(account, tx, settings, operatorId, feeMultiplier) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) @@ -91,102 +101,113 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) { .then(() => calculateFeeDiscount(feeMultiplier, unitScale)) .then(newFee => fetch('settxfee', [newFee])) .then(() => fetch('sendtoaddress', [toAddress, coins])) - .then((txId) => fetch('gettransaction', [txId])) - .then((res) => _.pick(['fee', 'txid'], res)) - .then((pickedObj) => { + .then(txId => fetch('gettransaction', [txId])) + .then(res => _.pick(['fee', 'txid'], res)) + .then(pickedObj => { return { fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, } }) .catch(errorHandle) } -function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) { +function sendCoinsBatch(account, txs, cryptoCode, feeMultiplier) { return checkCryptoCode(cryptoCode) .then(() => calculateFeeDiscount(feeMultiplier, unitScale)) .then(newFee => fetch('settxfee', [newFee])) - .then(() => _.reduce((acc, value) => ({ - ...acc, - [value.toAddress]: _.isNil(acc[value.toAddress]) - ? BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8) - : BN(acc[value.toAddress]).plus(BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8)) - }), {}, txs)) - .then((obj) => fetch('sendmany', ['', obj])) - .then((txId) => fetch('gettransaction', [txId])) - .then((res) => _.pick(['fee', 'txid'], res)) - .then((pickedObj) => ({ + .then(() => + _.reduce( + (acc, value) => ({ + ...acc, + [value.toAddress]: _.isNil(acc[value.toAddress]) + ? BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8) + : BN(acc[value.toAddress]).plus( + BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8), + ), + }), + {}, + txs, + ), + ) + .then(obj => fetch('sendmany', ['', obj])) + .then(txId => fetch('gettransaction', [txId])) + .then(res => _.pick(['fee', 'txid'], res)) + .then(pickedObj => ({ fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, })) .catch(errorHandle) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info) { return checkCryptoCode(info.cryptoCode) .then(() => fetch('getnewaddress')) .catch(errorHandle) } -function addressBalance (address, confs) { +function addressBalance(address, confs) { return fetch('getreceivedbyaddress', [address, confs]) .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0)) .catch(errorHandle) } -function confirmedBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 1)) +function confirmedBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 1)) } -function pendingBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 0)) +function pendingBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 0)) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => confirmedBalance(toAddress, cryptoCode)) .then(confirmed => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, cryptoCode) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, cryptoCode).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => { const promises = [ accountUnconfirmedBalance(cryptoCode), accountBalance(cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] return Promise.all(promises) }) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) + .then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) .catch(errorHandle) } -function cryptoNetwork (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main') +function cryptoNetwork(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => + parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main', + ) } -function fetchRBF (txId) { +function fetchRBF(txId) { return fetch('getmempoolentry', [txId]) - .then((res) => { + .then(res => { return [txId, res['bip125-replaceable']] }) .catch(err => { @@ -195,16 +216,23 @@ function fetchRBF (txId) { }) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getblockchaininfo')) - .then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready') + .then(res => (res['initialblockdownload'] ? 'syncing' : 'ready')) } -function getTxHashesByAddress (cryptoCode, address) { +function getTxHashesByAddress(cryptoCode, address) { checkCryptoCode(cryptoCode) .then(() => fetch('listreceivedbyaddress', [0, true, true, address])) - .then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress)))) + .then(txsByAddress => + Promise.all( + _.map( + id => fetch('getrawtransaction', [id]), + _.flatMap(it => it.txids, txsByAddress), + ), + ), + ) .then(_.map(({ hash }) => hash)) } @@ -220,5 +248,5 @@ module.exports = { checkBlockchainStatus, getTxHashesByAddress, fetch, - SUPPORTS_BATCHING + SUPPORTS_BATCHING, } diff --git a/packages/server/lib/plugins/wallet/bitgo/bitgo.js b/packages/server/lib/plugins/wallet/bitgo/bitgo.js index 749ee030..e20a5b8e 100644 --- a/packages/server/lib/plugins/wallet/bitgo/bitgo.js +++ b/packages/server/lib/plugins/wallet/bitgo/bitgo.js @@ -36,7 +36,7 @@ const getWallet = (account, cryptoCode) => { return bitgo.coin(coin).wallets().get({ id: walletId }) } -function checkCryptoCode (cryptoCode) { +function checkCryptoCode(cryptoCode) { if (!SUPPORTED_COINS.includes(cryptoCode)) { return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) } @@ -44,26 +44,26 @@ function checkCryptoCode (cryptoCode) { return Promise.resolve() } -function getLegacyAddress (address, cryptoCode) { +function getLegacyAddress(address, cryptoCode) { if (!BCH_CODES.includes(cryptoCode)) return address return toLegacyAddress(address) } -function getCashAddress (address, cryptoCode) { +function getCashAddress(address, cryptoCode) { if (!BCH_CODES.includes(cryptoCode)) return address return toCashAddress(address) } -function formatToGetStatus (address, cryptoCode) { +function formatToGetStatus(address, cryptoCode) { if (!BCH_CODES.includes(cryptoCode)) return address const [part1, part2] = getLegacyAddress(address, cryptoCode).split(':') return part2 || part1 } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => getWallet(account, cryptoCode)) @@ -72,7 +72,7 @@ function sendCoins (account, tx, settings, operatorId) { address: getLegacyAddress(toAddress, cryptoCode), amount: cryptoAtoms.toNumber(), walletPassphrase: account[`${cryptoCode}WalletPassphrase`], - enforceMinConfirmsForChange: false + enforceMinConfirmsForChange: false, } return wallet.send(params) }) @@ -83,51 +83,55 @@ function sendCoins (account, tx, settings, operatorId) { return { txid: txid, fee: new BN(fee).decimalPlaces(0) } }) .catch(err => { - if (err.message === 'insufficient funds') throw new E.InsufficientFundsError() + if (err.message === 'insufficient funds') + throw new E.InsufficientFundsError() throw err }) } -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => getWallet(account, cryptoCode)) .then(wallet => new BN(wallet._wallet.spendableBalanceString)) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info) { return checkCryptoCode(info.cryptoCode) .then(() => getWallet(account, info.cryptoCode)) .then(wallet => { - return wallet.createAddress() - .then(result => { - const address = result.address + return wallet.createAddress().then(result => { + const address = result.address - // If a label was provided, set the label - if (info.label) { - return wallet.updateAddress({ address: address, label: info.label }) - .then(() => getCashAddress(address, info.cryptoCode)) - } + // If a label was provided, set the label + if (info.label) { + return wallet + .updateAddress({ address: address, label: info.label }) + .then(() => getCashAddress(address, info.cryptoCode)) + } - return getCashAddress(address, info.cryptoCode) - }) + return getCashAddress(address, info.cryptoCode) + }) }) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => getWallet(account, cryptoCode)) - .then(wallet => wallet.transfers({ - type: 'receive', - address: formatToGetStatus(toAddress, cryptoCode) - })) + .then(wallet => + wallet.transfers({ + type: 'receive', + address: formatToGetStatus(toAddress, cryptoCode), + }), + ) .then(({ transfers }) => { - const filterConfirmed = _.filter(it => - it.state === 'confirmed' && it.type === 'receive' + const filterConfirmed = _.filter( + it => it.state === 'confirmed' && it.type === 'receive', ) - const filterPending = _.filter(it => - (it.state === 'confirmed' || it.state === 'unconfirmed') && - it.type === 'receive' + const filterPending = _.filter( + it => + (it.state === 'confirmed' || it.state === 'unconfirmed') && + it.type === 'receive', ) const sum = _.reduce((acc, val) => val.plus(acc), new BN(0)) @@ -136,40 +140,43 @@ function getStatus (account, tx, requested, settings, operatorId) { const confirmed = _.compose(sum, toBn, filterConfirmed)(transfers) const pending = _.compose(sum, toBn, filterPending)(transfers) - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } return { receivedCryptoAtoms: pending, status: 'notSeen' } }) } -function newFunding (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => { - return getWallet(account, cryptoCode) - .then(wallet => { - return wallet.createAddress() - .then(result => { - const fundingAddress = result.address - return wallet.updateAddress({ address: fundingAddress, label: 'Funding Address' }) - .then(() => ({ - fundingPendingBalance: new BN(wallet._wallet.balance).minus(wallet._wallet.confirmedBalance), - fundingConfirmedBalance: new BN(wallet._wallet.confirmedBalance), - fundingAddress: getCashAddress(fundingAddress, cryptoCode) - })) - }) - }) +function newFunding(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => { + return getWallet(account, cryptoCode).then(wallet => { + return wallet.createAddress().then(result => { + const fundingAddress = result.address + return wallet + .updateAddress({ address: fundingAddress, label: 'Funding Address' }) + .then(() => ({ + fundingPendingBalance: new BN(wallet._wallet.balance).minus( + wallet._wallet.confirmedBalance, + ), + fundingConfirmedBalance: new BN(wallet._wallet.confirmedBalance), + fundingAddress: getCashAddress(fundingAddress, cryptoCode), + })) + }) }) + }) } -function cryptoNetwork (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => account.environment === 'test' ? 'test' : 'main') +function cryptoNetwork(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => + account.environment === 'test' ? 'test' : 'main', + ) } -function checkBlockchainStatus (cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => Promise.resolve('ready')) +function checkBlockchainStatus(cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => Promise.resolve('ready')) } module.exports = { @@ -180,5 +187,5 @@ module.exports = { getStatus, newFunding, cryptoNetwork, - checkBlockchainStatus + checkBlockchainStatus, } diff --git a/packages/server/lib/plugins/wallet/dashd/dashd.js b/packages/server/lib/plugins/wallet/dashd/dashd.js index e5167332..ce4df134 100644 --- a/packages/server/lib/plugins/wallet/dashd/dashd.js +++ b/packages/server/lib/plugins/wallet/dashd/dashd.js @@ -11,11 +11,11 @@ const unitScale = cryptoRec.unitScale const rpcConfig = jsonRpc.rpcConfig(cryptoRec) -function fetch (method, params) { +function fetch(method, params) { return jsonRpc.fetch(rpcConfig, method, params) } -function errorHandle (e) { +function errorHandle(e) { const err = JSON.parse(e.message) switch (err.code) { case -6: @@ -25,110 +25,124 @@ function errorHandle (e) { } } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'DASH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'DASH') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } -function accountUnconfirmedBalance (cryptoCode) { +function accountUnconfirmedBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ unconfirmed_balance: balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } // We want a balance that includes all spends (0 conf) but only deposits that // have at least 1 confirmation. getbalance does this for us automatically. -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) return checkCryptoCode(cryptoCode) .then(() => fetch('sendtoaddress', [toAddress, coins])) - .then((txId) => fetch('gettransaction', [txId])) - .then((res) => _.pick(['fee', 'txid'], res)) - .then((pickedObj) => { + .then(txId => fetch('gettransaction', [txId])) + .then(res => _.pick(['fee', 'txid'], res)) + .then(pickedObj => { return { fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, } }) .catch(errorHandle) } -function newAddress (account, info, tx, settings, operatorId) { - return checkCryptoCode(info.cryptoCode) - .then(() => fetch('getnewaddress')) +function newAddress(account, info) { + return checkCryptoCode(info.cryptoCode).then(() => fetch('getnewaddress')) } -function addressBalance (address, confs) { - return fetch('getreceivedbyaddress', [address, confs]) - .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0)) +function addressBalance(address, confs) { + return fetch('getreceivedbyaddress', [address, confs]).then(r => + new BN(r).shiftedBy(unitScale).decimalPlaces(0), + ) } -function confirmedBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 1)) +function confirmedBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 1)) } -function pendingBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 0)) +function pendingBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 0)) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => confirmedBalance(toAddress, cryptoCode)) .then(confirmed => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, cryptoCode) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, cryptoCode).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => { const promises = [ accountUnconfirmedBalance(cryptoCode), accountBalance(cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] return Promise.all(promises) }) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) + .then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getblockchaininfo')) - .then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready') + .then(res => (res['initialblockdownload'] ? 'syncing' : 'ready')) } -function getTxHashesByAddress (cryptoCode, address) { +function getTxHashesByAddress(cryptoCode, address) { checkCryptoCode(cryptoCode) .then(() => fetch('listreceivedbyaddress', [0, true, true, true, address])) - .then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress)))) + .then(txsByAddress => + Promise.all( + _.map( + id => fetch('getrawtransaction', [id]), + _.flatMap(it => it.txids, txsByAddress), + ), + ), + ) .then(_.map(({ hash }) => hash)) } @@ -139,5 +153,5 @@ module.exports = { getStatus, newFunding, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/wallet/galoy/galoy.js b/packages/server/lib/plugins/wallet/galoy/galoy.js index a2e57906..9192d932 100644 --- a/packages/server/lib/plugins/wallet/galoy/galoy.js +++ b/packages/server/lib/plugins/wallet/galoy/galoy.js @@ -7,16 +7,16 @@ const SUPPORTED_COINS = ['LN'] const BN = require('../../../bn') -function request (graphqlQuery, token, endpoint) { +function request(graphqlQuery, token, endpoint) { const headers = { 'content-type': 'application/json', - 'X-API-KEY': token + 'X-API-KEY': token, } return axios({ method: 'post', url: endpoint, headers: headers, - data: graphqlQuery + data: graphqlQuery, }) .then(r => { if (r.error) throw r.error @@ -27,7 +27,7 @@ function request (graphqlQuery, token, endpoint) { }) } -function checkCryptoCode (cryptoCode) { +function checkCryptoCode(cryptoCode) { if (!SUPPORTED_COINS.includes(cryptoCode)) { return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) } @@ -35,10 +35,10 @@ function checkCryptoCode (cryptoCode) { return Promise.resolve() } -function getTransactionsByAddress (token, endpoint, walletId, address) { +function getTransactionsByAddress(token, endpoint, walletId, address) { const accountInfo = { - 'operationName': 'me', - 'query': `query me($walletId: WalletId!, , $address: OnChainAddress!) { + operationName: 'me', + query: `query me($walletId: WalletId!, , $address: OnChainAddress!) { me { defaultAccount { walletById(walletId: $walletId) { @@ -55,7 +55,7 @@ function getTransactionsByAddress (token, endpoint, walletId, address) { } } }`, - 'variables': { walletId, address } + variables: { walletId, address }, } return request(accountInfo, token, endpoint) .then(r => { @@ -66,10 +66,10 @@ function getTransactionsByAddress (token, endpoint, walletId, address) { }) } -function getGaloyWallet (token, endpoint, walletId) { +function getGaloyWallet(token, endpoint, walletId) { const accountInfo = { - 'operationName': 'me', - 'query': `query me($walletId: WalletId!) { + operationName: 'me', + query: `query me($walletId: WalletId!) { me { defaultAccount { walletById(walletId: $walletId) { @@ -80,7 +80,7 @@ function getGaloyWallet (token, endpoint, walletId) { } } }`, - 'variables': { walletId } + variables: { walletId }, } return request(accountInfo, token, endpoint) .then(r => { @@ -91,18 +91,18 @@ function getGaloyWallet (token, endpoint, walletId) { }) } -function isLnInvoice (address) { +function isLnInvoice(address) { return address.toLowerCase().startsWith('lnbc') } -function isLnurl (address) { +function isLnurl(address) { return address.toLowerCase().startsWith('lnurl') } -function sendFundsOnChain (walletId, address, cryptoAtoms, token, endpoint) { +function sendFundsOnChain(walletId, address, cryptoAtoms, token, endpoint) { const sendOnChain = { - 'operationName': 'onChainPaymentSend', - 'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { + operationName: 'onChainPaymentSend', + query: `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { onChainPaymentSend(input: $input) { errors { message @@ -111,18 +111,17 @@ function sendFundsOnChain (walletId, address, cryptoAtoms, token, endpoint) { status } }`, - 'variables': { 'input': { address, amount: cryptoAtoms.toString(), walletId } } + variables: { input: { address, amount: cryptoAtoms.toString(), walletId } }, } - return request(sendOnChain, token, endpoint) - .then(result => { - return result.data.onChainPaymentSend - }) + return request(sendOnChain, token, endpoint).then(result => { + return result.data.onChainPaymentSend + }) } -function sendFundsLNURL (walletId, lnurl, cryptoAtoms, token, endpoint) { +function sendFundsLNURL(walletId, lnurl, cryptoAtoms, token, endpoint) { const sendLnNoAmount = { - 'operationName': 'lnurlPaymentSend', - 'query': `mutation lnurlPaymentSend($input: LnurlPaymentSendInput!) { + operationName: 'lnurlPaymentSend', + query: `mutation lnurlPaymentSend($input: LnurlPaymentSendInput!) { lnurlPaymentSend(input: $input) { errors { message @@ -131,15 +130,23 @@ function sendFundsLNURL (walletId, lnurl, cryptoAtoms, token, endpoint) { status } }`, - 'variables': { 'input': { 'lnurl': `${lnurl}`, 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } } + variables: { + input: { + lnurl: `${lnurl}`, + walletId: `${walletId}`, + amount: `${cryptoAtoms}`, + }, + }, } - return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnurlPaymentSend) + return request(sendLnNoAmount, token, endpoint).then( + result => result.data.lnurlPaymentSend, + ) } -function sendFundsLN (walletId, invoice, cryptoAtoms, token, endpoint) { +function sendFundsLN(walletId, invoice, cryptoAtoms, token, endpoint) { const sendLnNoAmount = { - 'operationName': 'lnNoAmountInvoicePaymentSend', - 'query': `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { + operationName: 'lnNoAmountInvoicePaymentSend', + query: `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { lnNoAmountInvoicePaymentSend(input: $input) { errors { message @@ -148,15 +155,23 @@ function sendFundsLN (walletId, invoice, cryptoAtoms, token, endpoint) { status } }`, - 'variables': { 'input': { 'paymentRequest': invoice, walletId, amount: cryptoAtoms.toString() } } + variables: { + input: { + paymentRequest: invoice, + walletId, + amount: cryptoAtoms.toString(), + }, + }, } - return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoicePaymentSend) + return request(sendLnNoAmount, token, endpoint).then( + result => result.data.lnNoAmountInvoicePaymentSend, + ) } -function sendProbeRequest (walletId, invoice, cryptoAtoms, token, endpoint) { +function sendProbeRequest(walletId, invoice, cryptoAtoms, token, endpoint) { const sendProbeNoAmount = { - 'operationName': 'lnNoAmountInvoiceFeeProbe', - 'query': `mutation lnNoAmountInvoiceFeeProbe($input: LnNoAmountInvoiceFeeProbeInput!) { + operationName: 'lnNoAmountInvoiceFeeProbe', + query: `mutation lnNoAmountInvoiceFeeProbe($input: LnNoAmountInvoiceFeeProbeInput!) { lnNoAmountInvoiceFeeProbe(input: $input) { amount errors { @@ -165,22 +180,48 @@ function sendProbeRequest (walletId, invoice, cryptoAtoms, token, endpoint) { } } }`, - 'variables': { 'input': { paymentRequest: invoice, walletId, amount: cryptoAtoms.toString() } } + variables: { + input: { + paymentRequest: invoice, + walletId, + amount: cryptoAtoms.toString(), + }, + }, } - return request(sendProbeNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoiceFeeProbe) + return request(sendProbeNoAmount, token, endpoint).then( + result => result.data.lnNoAmountInvoiceFeeProbe, + ) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => { if (isLnInvoice(toAddress)) { - return sendFundsLN(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint) + return sendFundsLN( + account.walletId, + toAddress, + cryptoAtoms, + account.apiSecret, + account.endpoint, + ) } if (isLnurl(toAddress)) { - return sendFundsLNURL(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint) + return sendFundsLNURL( + account.walletId, + toAddress, + cryptoAtoms, + account.apiSecret, + account.endpoint, + ) } - return sendFundsOnChain(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint) + return sendFundsOnChain( + account.walletId, + toAddress, + cryptoAtoms, + account.apiSecret, + account.endpoint, + ) }) .then(result => { switch (result.status) { @@ -193,25 +234,33 @@ function sendCoins (account, tx, settings, operatorId) { case 'PENDING': return '' default: - throw new Error(`Transaction failed: ${_.head(result.errors).message}`) + throw new Error( + `Transaction failed: ${_.head(result.errors).message}`, + ) } }) } -function probeLN (account, cryptoCode, invoice) { +function probeLN(account, cryptoCode, invoice) { const probeHardLimits = [200000, 1000000, 2000000] const promises = probeHardLimits.map(limit => { - return sendProbeRequest(account.walletId, invoice, limit, account.apiSecret, account.endpoint) - .then(r => _.isEmpty(r.errors)) + return sendProbeRequest( + account.walletId, + invoice, + limit, + account.apiSecret, + account.endpoint, + ).then(r => _.isEmpty(r.errors)) }) - return Promise.all(promises) - .then(results => _.zipObject(probeHardLimits, results)) + return Promise.all(promises).then(results => + _.zipObject(probeHardLimits, results), + ) } -function newOnChainAddress (walletId, token, endpoint) { +function newOnChainAddress(walletId, token, endpoint) { const createOnChainAddress = { - 'operationName': 'onChainAddressCreate', - 'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) { + operationName: 'onChainAddressCreate', + query: `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) { onChainAddressCreate(input: $input) { address errors { @@ -220,41 +269,17 @@ function newOnChainAddress (walletId, token, endpoint) { } } }`, - 'variables': { 'input': { walletId } } + variables: { input: { walletId } }, } - return request(createOnChainAddress, token, endpoint) - .then(result => { - return result.data.onChainAddressCreate.address - }) + return request(createOnChainAddress, token, endpoint).then(result => { + return result.data.onChainAddressCreate.address + }) } -function newNoAmountInvoice (walletId, token, endpoint) { +function newInvoice(walletId, cryptoAtoms, token, endpoint) { const createInvoice = { - 'operationName': 'lnNoAmountInvoiceCreate', - 'query': `mutation lnNoAmountInvoiceCreate($input: LnNoAmountInvoiceCreateInput!) { - lnNoAmountInvoiceCreate(input: $input) { - errors { - message - path - } - invoice { - paymentRequest - } - } - }`, - 'variables': { 'input': { walletId } } - } - return request(createInvoice, token, endpoint) - .then(result => { - return result.data.lnNoAmountInvoiceCreate.invoice.paymentRequest - }) - -} - -function newInvoice (walletId, cryptoAtoms, token, endpoint) { - const createInvoice = { - 'operationName': 'lnInvoiceCreate', - 'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { + operationName: 'lnInvoiceCreate', + query: `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { lnInvoiceCreate(input: $input) { errors { message @@ -265,37 +290,44 @@ function newInvoice (walletId, cryptoAtoms, token, endpoint) { } } }`, - 'variables': { 'input': { walletId, amount: cryptoAtoms.toString() } } + variables: { input: { walletId, amount: cryptoAtoms.toString() } }, } - return request(createInvoice, token, endpoint) - .then(result => { - return result.data.lnInvoiceCreate.invoice.paymentRequest - }) + return request(createInvoice, token, endpoint).then(result => { + return result.data.lnInvoiceCreate.invoice.paymentRequest + }) } -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return checkCryptoCode(cryptoCode) - .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId)) + .then(() => + getGaloyWallet(account.apiSecret, account.endpoint, account.walletId), + ) .then(wallet => { return new BN(wallet.balance || 0) }) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info, tx) { const { cryptoAtoms, cryptoCode } = tx - return checkCryptoCode(cryptoCode) - .then(() => newInvoice(account.walletId, cryptoAtoms, account.apiSecret, account.endpoint)) + return checkCryptoCode(cryptoCode).then(() => + newInvoice( + account.walletId, + cryptoAtoms, + account.apiSecret, + account.endpoint, + ), + ) } -function getInvoiceStatus (token, endpoint, address) { +function getInvoiceStatus(token, endpoint, address) { const query = { - 'operationName': 'lnInvoicePaymentStatus', - 'query': `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) { + operationName: 'lnInvoicePaymentStatus', + query: `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) { lnInvoicePaymentStatus(input: $input) { status } }`, - 'variables': { input: { paymentRequest: address } } + variables: { input: { paymentRequest: address } }, } return request(query, token, endpoint) .then(r => { @@ -306,62 +338,89 @@ function getInvoiceStatus (token, endpoint, address) { }) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoAtoms, cryptoCode } = tx - const getBalance = _.reduce((acc, value) => { - acc[value.node.status] = acc[value.node.status].plus(new BN(value.node.settlementAmount)) - return acc - }, { SUCCESS: new BN(0), PENDING: new BN(0), FAILURE: new BN(0) }) + const getBalance = _.reduce( + (acc, value) => { + acc[value.node.status] = acc[value.node.status].plus( + new BN(value.node.settlementAmount), + ) + return acc + }, + { SUCCESS: new BN(0), PENDING: new BN(0), FAILURE: new BN(0) }, + ) - return checkCryptoCode(cryptoCode) - .then(() => { - const address = coinUtils.parseUrl(cryptoCode, account.environment, toAddress, false) - if (isLnInvoice(address)) { - return getInvoiceStatus(account.apiSecret, account.endpoint, address) - .then(it => { - const isPaid = it === 'PAID' - if (isPaid) return { receivedCryptoAtoms: cryptoAtoms, status: 'confirmed' } - return { receivedCryptoAtoms: BN(0), status: 'notSeen' } - }) - } - // On-chain and intra-ledger transactions - return getTransactionsByAddress(account.apiSecret, account.endpoint, account.walletId, address) - .then(transactions => { - const { SUCCESS: confirmed, PENDING: pending } = getBalance(transactions.edges) - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return checkCryptoCode(cryptoCode).then(() => { + const address = coinUtils.parseUrl( + cryptoCode, + account.environment, + toAddress, + false, + ) + if (isLnInvoice(address)) { + return getInvoiceStatus( + account.apiSecret, + account.endpoint, + address, + ).then(it => { + const isPaid = it === 'PAID' + if (isPaid) + return { receivedCryptoAtoms: cryptoAtoms, status: 'confirmed' } + return { receivedCryptoAtoms: BN(0), status: 'notSeen' } + }) + } + // On-chain and intra-ledger transactions + return getTransactionsByAddress( + account.apiSecret, + account.endpoint, + account.walletId, + address, + ).then(transactions => { + const { SUCCESS: confirmed, PENDING: pending } = getBalance( + transactions.edges, + ) + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } }) + }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { // Regular BTC address return checkCryptoCode(cryptoCode) - .then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId)) + .then(() => + getGaloyWallet(account.apiSecret, account.endpoint, account.walletId), + ) .then(wallet => { - return newOnChainAddress(account.walletId, account.apiSecret, account.endpoint) - .then(onChainAddress => [onChainAddress, wallet.balance]) + return newOnChainAddress( + account.walletId, + account.apiSecret, + account.endpoint, + ).then(onChainAddress => [onChainAddress, wallet.balance]) }) .then(([onChainAddress, balance]) => { return { // with the old api is not possible to get pending balance fundingPendingBalance: new BN(0), fundingConfirmedBalance: new BN(balance), - fundingAddress: onChainAddress + fundingAddress: onChainAddress, } }) } -function cryptoNetwork (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => account.environment === 'test' ? 'test' : 'main') +function cryptoNetwork(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => + account.environment === 'test' ? 'test' : 'main', + ) } -function checkBlockchainStatus (cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => Promise.resolve('ready')) +function checkBlockchainStatus(cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => Promise.resolve('ready')) } module.exports = { @@ -373,5 +432,5 @@ module.exports = { newFunding, cryptoNetwork, checkBlockchainStatus, - probeLN + probeLN, } diff --git a/packages/server/lib/plugins/wallet/geth/base.js b/packages/server/lib/plugins/wallet/geth/base.js index 1b8d91ce..15f0fe5c 100644 --- a/packages/server/lib/plugins/wallet/geth/base.js +++ b/packages/server/lib/plugins/wallet/geth/base.js @@ -6,7 +6,6 @@ const web3 = new Web3() const hdkey = require('ethereumjs-wallet/hdkey') const { FeeMarketEIP1559Transaction } = require('@ethereumjs/tx') const { default: Common, Chain, Hardfork } = require('@ethereumjs/common') -const Tx = require('ethereumjs-tx') const { default: PQueue } = require('p-queue') const util = require('ethereumjs-util') const coins = require('@lamassu/coins') @@ -34,7 +33,7 @@ module.exports = { connect, checkBlockchainStatus, getTxHashesByAddress, - _balance + _balance, } const SWEEP_QUEUE = new PQueue({ @@ -55,93 +54,122 @@ const pify = _function => { const logInfuraCall = call => { if (!_.includes('infura', web3.currentProvider.host)) return - _.isNil(infuraCalls[call]) ? infuraCalls[call] = 1 : infuraCalls[call]++ - logger.info(`Calling web3 method ${call} via Infura. Current count for this session: ${JSON.stringify(infuraCalls)}`) + _.isNil(infuraCalls[call]) ? (infuraCalls[call] = 1) : infuraCalls[call]++ + logger.info( + `Calling web3 method ${call} via Infura. Current count for this session: ${JSON.stringify(infuraCalls)}`, + ) } -function connect (url) { +function connect(url) { web3.setProvider(new web3.providers.HttpProvider(url)) } const hex = bigNum => '0x' + bigNum.integerValue(BN.ROUND_DOWN).toString(16) -function privateKey (account) { +function privateKey(account) { return defaultWallet(account).getPrivateKey() } -function isStrictAddress (cryptoCode, toAddress, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => util.isValidChecksumAddress(toAddress)) +function isStrictAddress(cryptoCode, toAddress) { + return checkCryptoCode(cryptoCode).then(() => + util.isValidChecksumAddress(toAddress), + ) } -function getTxHashesByAddress (cryptoCode, address) { - throw new Error(`Transactions hash retrieval is not implemented for this coin!`) +function getTxHashesByAddress() { + throw new Error( + `Transactions hash retrieval is not implemented for this coin!`, + ) } -function sendCoins (account, tx, settings, operatorId, feeMultiplier) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx const isErc20Token = coins.utils.isErc20Token(cryptoCode) return SEND_QUEUE.add(() => - (isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) + (isErc20Token ? generateErc20Tx : generateTx)( + toAddress, + defaultWallet(account), + cryptoAtoms, + false, + cryptoCode, + ) .then(pify(web3.eth.sendSignedTransaction)) .then(txid => { - return pify(web3.eth.getTransaction)(txid) - .then(tx => { - if (!tx) return { txid } + return pify(web3.eth.getTransaction)(txid).then(tx => { + if (!tx) return { txid } - const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0) + const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0) - return { txid, fee } - }) - }) + return { txid, fee } + }) + }), ) } -function checkCryptoCode (cryptoCode) { +function checkCryptoCode(cryptoCode) { if (cryptoCode === 'ETH' || coins.utils.isErc20Token(cryptoCode)) { return Promise.resolve(cryptoCode) } return Promise.reject(new Error('cryptoCode must be ETH')) } -function balance (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(code => confirmedBalance(defaultAddress(account), code)) +function balance(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(code => + confirmedBalance(defaultAddress(account), code), + ) } const pendingBalance = (address, cryptoCode) => { - const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)] - return Promise.all(promises).then(([pending, confirmed]) => BN(pending).minus(confirmed)) + const promises = [ + _balance(true, address, cryptoCode), + _balance(false, address, cryptoCode), + ] + return Promise.all(promises).then(([pending, confirmed]) => + BN(pending).minus(confirmed), + ) } -const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode) +const confirmedBalance = (address, cryptoCode) => + _balance(false, address, cryptoCode) -function _balance (includePending, address, cryptoCode) { +function _balance(includePending, address, cryptoCode) { if (coins.utils.isErc20Token(cryptoCode)) { - const contract = new web3.eth.Contract(ABI.ERC20, coins.utils.getErc20Token(cryptoCode).contractAddress) - return contract.methods.balanceOf(address.toLowerCase()).call((_, balance) => { - return contract.methods.decimals().call((_, decimals) => BN(balance).div(10 ** decimals)) - }) + const contract = new web3.eth.Contract( + ABI.ERC20, + coins.utils.getErc20Token(cryptoCode).contractAddress, + ) + return contract.methods + .balanceOf(address.toLowerCase()) + .call((_, balance) => { + return contract.methods + .decimals() + .call((_, decimals) => BN(balance).div(10 ** decimals)) + }) } const block = includePending ? 'pending' : undefined - return pify(web3.eth.getBalance)(address.toLowerCase(), block) - /* NOTE: Convert bn.js bignum to bignumber.js bignum */ - .then(balance => balance ? BN(balance) : BN(0)) + return ( + pify(web3.eth.getBalance)(address.toLowerCase(), block) + /* NOTE: Convert bn.js bignum to bignumber.js bignum */ + .then(balance => (balance ? BN(balance) : BN(0))) + ) } -function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) { +function generateErc20Tx(_toAddress, wallet, amount, includesFee, cryptoCode) { const fromAddress = '0x' + wallet.getAddress().toString('hex') const toAddress = coins.utils.getErc20Token(cryptoCode).contractAddress const contract = new web3.eth.Contract(ABI.ERC20, toAddress) - const contractData = contract.methods.transfer(_toAddress.toLowerCase(), hex(amount)) + const contractData = contract.methods.transfer( + _toAddress.toLowerCase(), + hex(amount), + ) const txTemplate = { from: fromAddress, to: toAddress, value: hex(BN(0)), - data: contractData.encodeABI() + data: contractData.encodeABI(), } const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) @@ -149,24 +177,22 @@ function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) { const promises = [ pify(contractData.estimateGas)(txTemplate), pify(web3.eth.getTransactionCount)(fromAddress), - pify(web3.eth.getBlock)('pending') + pify(web3.eth.getBlock)('pending'), ] return Promise.all(promises) .then(([gas, txCount, { baseFeePerGas }]) => [ BN(gas), _.max([0, txCount, lastUsedNonces[fromAddress] + 1]), - BN(baseFeePerGas) + BN(baseFeePerGas), ]) .then(([gas, txCount, baseFeePerGas]) => { lastUsedNonces[fromAddress] = txCount const maxPriorityFeePerGas = new BN(web3.utils.toWei('1.0', 'gwei')) // web3 default value - const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas) - - if (includesFee && (toSend.isNegative() || toSend.isZero())) { - throw new Error(`Trying to send a nil or negative amount (Transaction ID: ${txId} | Value provided: ${toSend.toNumber()}). This is probably caused due to the estimated fee being higher than the address' balance.`) - } + const maxFeePerGas = new BN(2) + .times(baseFeePerGas) + .plus(maxPriorityFeePerGas) const rawTx = { chainId: 1, @@ -177,7 +203,7 @@ function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) { to: toAddress, from: fromAddress, value: hex(BN(0)), - data: contractData.encodeABI() + data: contractData.encodeABI(), } const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common }) @@ -189,7 +215,7 @@ function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) { }) } -function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) { +function generateTx(_toAddress, wallet, amount, includesFee) { const fromAddress = '0x' + wallet.getAddress().toString('hex') const toAddress = _toAddress.toLowerCase() @@ -197,7 +223,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) const txTemplate = { from: fromAddress, to: toAddress, - value: amount.toString() + value: amount.toString(), } const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) @@ -206,7 +232,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) pify(web3.eth.estimateGas)(txTemplate), pify(web3.eth.getGasPrice)(), pify(web3.eth.getTransactionCount)(fromAddress), - pify(web3.eth.getBlock)('pending') + pify(web3.eth.getBlock)('pending'), ] return Promise.all(promises) @@ -214,9 +240,9 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) BN(gas), BN(gasPrice), _.max([0, txCount, lastUsedNonces[fromAddress] + 1]), - BN(baseFeePerGas) + BN(baseFeePerGas), ]) - .then(([gas, gasPrice, txCount, baseFeePerGas]) => { + .then(([gas, , txCount, baseFeePerGas]) => { lastUsedNonces[fromAddress] = txCount const maxPriorityFeePerGas = new BN(web3.utils.toWei('1.0', 'gwei')) // web3 default value @@ -234,7 +260,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) gasLimit: hex(gas), to: toAddress, from: fromAddress, - value: hex(toSend) + value: hex(toSend), } const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common }) @@ -246,85 +272,97 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) }) } -function defaultWallet (account) { +function defaultWallet(account) { return defaultHdNode(account).deriveChild(0).getWallet() } -function defaultAddress (account) { +function defaultAddress(account) { return defaultWallet(account).getChecksumAddressString() } -function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) { +function sweep(account, txId, cryptoCode, hdIndex) { const wallet = paymentHdNode(account).deriveChild(hdIndex).getWallet() const fromAddress = wallet.getChecksumAddressString() - return SWEEP_QUEUE.add(() => confirmedBalance(fromAddress, cryptoCode) - .then(r => { + return SWEEP_QUEUE.add(() => + confirmedBalance(fromAddress, cryptoCode).then(r => { if (r.eq(0)) return - return generateTx(defaultAddress(account), wallet, r, true, cryptoCode, txId) - .then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx)) - }) + return generateTx( + defaultAddress(account), + wallet, + r, + true, + cryptoCode, + txId, + ).then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx)) + }), ) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info) { const childNode = paymentHdNode(account).deriveChild(info.hdIndex) return Promise.resolve(childNode.getWallet().getChecksumAddressString()) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(code => Promise.all([confirmedBalance(toAddress, code), code])) .then(([confirmed, code]) => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, code) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'published' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, code).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'published' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function paymentHdNode (account) { +function paymentHdNode(account) { const masterSeed = account.seed if (!masterSeed) throw new Error('No master seed!') const key = hdkey.fromMasterSeed(masterSeed) return key.derivePath(paymentPrefixPath) } -function defaultHdNode (account) { +function defaultHdNode(account) { const masterSeed = account.seed if (!masterSeed) throw new Error('No master seed!') const key = hdkey.fromMasterSeed(masterSeed) return key.derivePath(defaultPrefixPath) } -function newFunding (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(code => { - const fundingAddress = defaultAddress(account) +function newFunding(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(code => { + const fundingAddress = defaultAddress(account) - const promises = [ - pendingBalance(fundingAddress, code), - confirmedBalance(fundingAddress, code) - ] + const promises = [ + pendingBalance(fundingAddress, code), + confirmedBalance(fundingAddress, code), + ] - return Promise.all(promises) - .then(([fundingPendingBalance, fundingConfirmedBalance]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) - }) + return Promise.all(promises).then( + ([fundingPendingBalance, fundingConfirmedBalance]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) + }) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) - .then(() => connect(`http://localhost:${coins.utils.getCryptoCurrency(cryptoCode).defaultPort}`)) + .then(() => + connect( + `http://localhost:${coins.utils.getCryptoCurrency(cryptoCode).defaultPort}`, + ), + ) .then(() => web3.eth.syncing) - .then(res => res === false ? 'ready' : 'syncing') + .then(res => (res === false ? 'ready' : 'syncing')) } diff --git a/packages/server/lib/plugins/wallet/geth/geth.js b/packages/server/lib/plugins/wallet/geth/geth.js index 61b28e73..a093e158 100644 --- a/packages/server/lib/plugins/wallet/geth/geth.js +++ b/packages/server/lib/plugins/wallet/geth/geth.js @@ -8,7 +8,7 @@ const defaultPort = cryptoRec.defaultPort const NAME = 'geth' -function run (account) { +function run() { base.connect(`http://localhost:${defaultPort}`) } diff --git a/packages/server/lib/plugins/wallet/infura/infura.js b/packages/server/lib/plugins/wallet/infura/infura.js index 5732bc2c..30188f74 100644 --- a/packages/server/lib/plugins/wallet/infura/infura.js +++ b/packages/server/lib/plugins/wallet/infura/infura.js @@ -6,11 +6,13 @@ const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('../../../constants') const NAME = 'infura' -function run (account) { - if (!account.endpoint) throw new Error('Need to configure API endpoint for Infura') +function run(account) { + if (!account.endpoint) + throw new Error('Need to configure API endpoint for Infura') const endpoint = _.startsWith('https://')(account.endpoint) - ? account.endpoint : `https://${account.endpoint}` + ? account.endpoint + : `https://${account.endpoint}` base.connect(endpoint) } @@ -18,24 +20,35 @@ function run (account) { const txsCache = new NodeCache({ stdTTL: T.hour / 1000, checkperiod: T.minute / 1000, - deleteOnExpire: true + deleteOnExpire: true, }) -function shouldGetStatus (tx) { +function shouldGetStatus(tx) { const timePassedSinceTx = Date.now() - new Date(tx.created) - const timePassedSinceReq = Date.now() - new Date(txsCache.get(tx.id).lastReqTime) + const timePassedSinceReq = + Date.now() - new Date(txsCache.get(tx.id).lastReqTime) - if (timePassedSinceTx < 3 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 10 * T.seconds - if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 20 * T.seconds - if (timePassedSinceTx < 30 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.minute - if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * T.minute - if (timePassedSinceTx < 3 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 5 * T.minute - if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour + if (timePassedSinceTx < 3 * T.minutes) + return ( + _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 10 * T.seconds + ) + if (timePassedSinceTx < 5 * T.minutes) + return ( + _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 20 * T.seconds + ) + if (timePassedSinceTx < 30 * T.minutes) + return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.minute + if (timePassedSinceTx < 1 * T.hour) + return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * T.minute + if (timePassedSinceTx < 3 * T.hours) + return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 5 * T.minute + if (timePassedSinceTx < 1 * T.day) + return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour } // Override geth's getStatus function to allow for different polling timing -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested, settings, operatorId) { if (_.isNil(txsCache.get(tx.id))) { txsCache.set(tx.id, { lastReqTime: Date.now() }) } @@ -45,7 +58,8 @@ function getStatus (account, tx, requested, settings, operatorId) { return Promise.resolve(txsCache.get(tx.id).res) } - return base.getStatus(account, tx, requested, settings, operatorId) + return base + .getStatus(account, tx, requested, settings, operatorId) .then(res => { if (res.status === 'confirmed') { txsCache.del(tx.id) // Transaction reached final status, can trim it from the caching obj @@ -57,4 +71,9 @@ function getStatus (account, tx, requested, settings, operatorId) { }) } -module.exports = _.merge(base, { NAME, run, getStatus, fetchSpeed: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW }) +module.exports = _.merge(base, { + NAME, + run, + getStatus, + fetchSpeed: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW, +}) diff --git a/packages/server/lib/plugins/wallet/litecoind/litecoind.js b/packages/server/lib/plugins/wallet/litecoind/litecoind.js index 61dc1f9e..8443ebd6 100644 --- a/packages/server/lib/plugins/wallet/litecoind/litecoind.js +++ b/packages/server/lib/plugins/wallet/litecoind/litecoind.js @@ -11,11 +11,11 @@ const unitScale = cryptoRec.unitScale const rpcConfig = jsonRpc.rpcConfig(cryptoRec) -function fetch (method, params) { +function fetch(method, params) { return jsonRpc.fetch(rpcConfig, method, params) } -function errorHandle (e) { +function errorHandle(e) { const err = JSON.parse(e.message) switch (err.code) { case -6: @@ -25,107 +25,114 @@ function errorHandle (e) { } } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'LTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'LTC') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } -function accountUnconfirmedBalance (cryptoCode) { +function accountUnconfirmedBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ unconfirmed_balance: balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } // We want a balance that includes all spends (0 conf) but only deposits that // have at least 1 confirmation. getbalance does this for us automatically. -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) return checkCryptoCode(cryptoCode) .then(() => fetch('sendtoaddress', [toAddress, coins])) - .then((txId) => fetch('gettransaction', [txId])) - .then((res) => _.pick(['fee', 'txid'], res)) - .then((pickedObj) => { + .then(txId => fetch('gettransaction', [txId])) + .then(res => _.pick(['fee', 'txid'], res)) + .then(pickedObj => { return { fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, } }) .catch(errorHandle) } -function newAddress (account, info, tx, settings, operatorId) { - return checkCryptoCode(info.cryptoCode) - .then(() => fetch('getnewaddress')) +function newAddress(account, info) { + return checkCryptoCode(info.cryptoCode).then(() => fetch('getnewaddress')) } -function addressBalance (address, confs) { - return fetch('getreceivedbyaddress', [address, confs]) - .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0)) +function addressBalance(address, confs) { + return fetch('getreceivedbyaddress', [address, confs]).then(r => + new BN(r).shiftedBy(unitScale).decimalPlaces(0), + ) } -function confirmedBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 1)) +function confirmedBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 1)) } -function pendingBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 0)) +function pendingBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 0)) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => confirmedBalance(toAddress, cryptoCode)) .then(confirmed => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, cryptoCode) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, cryptoCode).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => { const promises = [ accountUnconfirmedBalance(cryptoCode), accountBalance(cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] return Promise.all(promises) }) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) + .then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getblockchaininfo')) - .then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready') + .then(res => (res['initialblockdownload'] ? 'syncing' : 'ready')) } -function getTxHashesByAddress (cryptoCode, address) { +function getTxHashesByAddress() { throw new Error(`Transactions hash retrieval not implemented for this coin!`) } @@ -136,5 +143,5 @@ module.exports = { getStatus, newFunding, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/wallet/mock-wallet/mock-wallet.js b/packages/server/lib/plugins/wallet/mock-wallet/mock-wallet.js index 4e359287..5dfac8d5 100644 --- a/packages/server/lib/plugins/wallet/mock-wallet/mock-wallet.js +++ b/packages/server/lib/plugins/wallet/mock-wallet/mock-wallet.js @@ -14,118 +14,150 @@ const SUPPORTED_COINS = coinUtils.cryptoCurrencies() let t0 -const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS) - ? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) - : Promise.resolve() +const checkCryptoCode = cryptoCode => + !_.includes(cryptoCode, SUPPORTED_COINS) + ? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) + : Promise.resolve() -function _balance (cryptoCode) { +function _balance(cryptoCode) { const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale return new BN(10).shiftedBy(unitScale).decimalPlaces(0) } -function balance (account, cryptoCode, settings, operatorId) { - return Promise.resolve() - .then(() => _balance(cryptoCode)) +function balance(account, cryptoCode) { + return Promise.resolve().then(() => _balance(cryptoCode)) } -function pendingBalance (account, cryptoCode) { - return balance(account, cryptoCode) - .then(b => b.times(1.1)) +function pendingBalance(account, cryptoCode) { + return balance(account, cryptoCode).then(b => b.times(1.1)) } -function confirmedBalance (account, cryptoCode) { +function confirmedBalance(account, cryptoCode) { return balance(account, cryptoCode) } // Note: This makes it easier to test insufficient funds errors let sendCount = 100 -function isInsufficient (cryptoAtoms, cryptoCode) { +function isInsufficient(cryptoAtoms, cryptoCode) { const b = _balance(cryptoCode) return cryptoAtoms.gt(b.div(1000).times(sendCount)) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx sendCount++ return new Promise((resolve, reject) => { setTimeout(() => { if (isInsufficient(cryptoAtoms, cryptoCode)) { - console.log('[%s] DEBUG: Mock wallet insufficient funds: %s', - cryptoCode, cryptoAtoms.toString()) + console.log( + '[%s] DEBUG: Mock wallet insufficient funds: %s', + cryptoCode, + cryptoAtoms.toString(), + ) return reject(new E.InsufficientFundsError()) } - console.log('[%s] DEBUG: Mock wallet sending %s cryptoAtoms to %s', - cryptoCode, cryptoAtoms.toString(), toAddress) + console.log( + '[%s] DEBUG: Mock wallet sending %s cryptoAtoms to %s', + cryptoCode, + cryptoAtoms.toString(), + toAddress, + ) return resolve({ txid: '', fee: new BN(0) }) }, 2000) }) } -function sendCoinsBatch (account, txs, cryptoCode) { +function sendCoinsBatch(account, txs, cryptoCode) { sendCount = sendCount + txs.length return new Promise((resolve, reject) => { setTimeout(() => { - const cryptoSum = _.reduce((acc, value) => acc.plus(value.crypto_atoms), BN(0), txs) + const cryptoSum = _.reduce( + (acc, value) => acc.plus(value.crypto_atoms), + BN(0), + txs, + ) if (isInsufficient(cryptoSum, cryptoCode)) { - console.log('[%s] DEBUG: Mock wallet insufficient funds: %s', - cryptoCode, cryptoSum.toString()) + console.log( + '[%s] DEBUG: Mock wallet insufficient funds: %s', + cryptoCode, + cryptoSum.toString(), + ) return reject(new E.InsufficientFundsError()) } - console.log('[%s] DEBUG: Mock wallet sending %s cryptoAtoms in a batch', - cryptoCode, cryptoSum.toString()) + console.log( + '[%s] DEBUG: Mock wallet sending %s cryptoAtoms in a batch', + cryptoCode, + cryptoSum.toString(), + ) return resolve({ txid: '', fee: BN(0) }) }, 2000) }) } -function newAddress () { +function newAddress() { t0 = Date.now() - return Promise.resolve('') + return Promise.resolve("") } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { const promises = [ pendingBalance(account, cryptoCode), confirmedBalance(account, cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] - return Promise.all(promises) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + return Promise.all(promises).then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ fundingPendingBalance, fundingConfirmedBalance, - fundingAddress - })) + fundingAddress, + }), + ) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx const elapsed = Date.now() - t0 - if (elapsed < PUBLISH_TIME) return Promise.resolve({ receivedCryptoAtoms: new BN(0), status: 'notSeen' }) - if (elapsed < AUTHORIZE_TIME) return Promise.resolve({ receivedCryptoAtoms: requested, status: 'published' }) - if (elapsed < CONFIRM_TIME) return Promise.resolve({ receivedCryptoAtoms: requested, status: 'authorized' }) + if (elapsed < PUBLISH_TIME) + return Promise.resolve({ + receivedCryptoAtoms: new BN(0), + status: 'notSeen', + }) + if (elapsed < AUTHORIZE_TIME) + return Promise.resolve({ + receivedCryptoAtoms: requested, + status: 'published', + }) + if (elapsed < CONFIRM_TIME) + return Promise.resolve({ + receivedCryptoAtoms: requested, + status: 'authorized', + }) - console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5)) + console.log( + '[%s] DEBUG: Mock wallet has confirmed transaction [%s]', + cryptoCode, + toAddress.slice(0, 5), + ) return Promise.resolve({ status: 'confirmed' }) } -function getTxHashesByAddress (cryptoCode, address) { - return new Promise((resolve, reject) => { +function getTxHashesByAddress() { + return new Promise(resolve => { setTimeout(() => { return resolve([]) // TODO: should return something other than empty list? }, 100) }) } -function checkBlockchainStatus (cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => Promise.resolve('ready')) +function checkBlockchainStatus(cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => Promise.resolve('ready')) } module.exports = { @@ -137,5 +169,5 @@ module.exports = { getStatus, newFunding, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/wallet/monerod/monerod.js b/packages/server/lib/plugins/wallet/monerod/monerod.js index d5aa5098..36274ba0 100644 --- a/packages/server/lib/plugins/wallet/monerod/monerod.js +++ b/packages/server/lib/plugins/wallet/monerod/monerod.js @@ -13,59 +13,61 @@ const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR const cryptoRec = utils.getCryptoCurrency(COINS.XMR) const configPath = utils.configPath(cryptoRec, BLOCKCHAIN_DIR) -const walletDir = path.resolve(utils.cryptoDir(cryptoRec, BLOCKCHAIN_DIR), 'wallets') +const walletDir = path.resolve( + utils.cryptoDir(cryptoRec, BLOCKCHAIN_DIR), + 'wallets', +) const DIGEST_QUEUE = new PQueue({ concurrency: 1, interval: 150, }) -function createDigestRequest (account = {}, method, params = []) { - return DIGEST_QUEUE.add(() => jsonRpc.fetchDigest(account, method, params) - .then(res => { +function createDigestRequest(account = {}, method, params = []) { + return DIGEST_QUEUE.add(() => + jsonRpc.fetchDigest(account, method, params).then(res => { const r = JSON.parse(res) if (r.error) throw r.error return r.result - }) + }), ) } -function rpcConfig () { +function rpcConfig() { try { const config = jsonRpc.parseConf(configPath) return { username: config['rpc-login'].split(':')[0], password: config['rpc-login'].split(':')[1], - port: cryptoRec.walletPort || cryptoRec.defaultPort + port: cryptoRec.walletPort || cryptoRec.defaultPort, } } catch (err) { - logger.error('Wallet is currently not installed!') + logger.error(`Wallet is currently not installed! ${err}`) return { username: '', password: '', - port: cryptoRec.walletPort || cryptoRec.defaultPort + port: cryptoRec.walletPort || cryptoRec.defaultPort, } } } -function fetch (method, params) { +function fetch(method, params) { return createDigestRequest(rpcConfig(), method, params) } -function handleError (error, method) { - switch(error.code) { - case -13: - { - if ( - fs.existsSync(path.resolve(walletDir, 'Wallet')) && - fs.existsSync(path.resolve(walletDir, 'Wallet.keys')) - ) { - logger.debug('Found wallet! Opening wallet...') - return openWallet() - } - logger.debug('Couldn\'t find wallet! Creating...') - return createWallet() +function handleError(error, method) { + switch (error.code) { + case -13: { + if ( + fs.existsSync(path.resolve(walletDir, 'Wallet')) && + fs.existsSync(path.resolve(walletDir, 'Wallet.keys')) + ) { + logger.debug('Found wallet! Opening wallet...') + return openWallet() } + logger.debug("Couldn't find wallet! Creating...") + return createWallet() + } case -21: throw new Error('Wallet already exists!') case -22: @@ -83,56 +85,63 @@ function handleError (error, method) { _.join(' ', [ `json-rpc::${method} error:`, JSON.stringify(_.get('message', error, '')), - JSON.stringify(_.get('response.data.error', error, '')) - ]) + JSON.stringify(_.get('response.data.error', error, '')), + ]), ) } } -function openWallet () { - return fetch('open_wallet', { filename: 'Wallet' }) - .catch(() => openWalletWithPassword()) +function openWallet() { + return fetch('open_wallet', { filename: 'Wallet' }).catch(() => + openWalletWithPassword(), + ) } -function openWalletWithPassword () { - return fetch('open_wallet', { filename: 'Wallet', password: rpcConfig().password }) +function openWalletWithPassword() { + return fetch('open_wallet', { + filename: 'Wallet', + password: rpcConfig().password, + }) } -function createWallet () { +function createWallet() { return fetch('create_wallet', { filename: 'Wallet', language: 'English' }) .then(() => new Promise(() => setTimeout(() => openWallet(), 3000))) .then(() => fetch('auto_refresh')) } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'XMR') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'XMR') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function refreshWallet () { - return fetch('refresh') - .catch(err => handleError(err, 'refreshWallet')) +function refreshWallet() { + return fetch('refresh').catch(err => handleError(err, 'refreshWallet')) } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => refreshWallet()) - .then(() => fetch('get_balance', { account_index: 0, address_indices: [0] })) + .then(() => + fetch('get_balance', { account_index: 0, address_indices: [0] }), + ) .then(res => { return BN(res.unlocked_balance).decimalPlaces(0) }) .catch(err => handleError(err, 'accountBalance')) } -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId, feeMultiplier) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => refreshWallet()) - .then(() => fetch('transfer_split', { + .then(() => + fetch('transfer_split', { destinations: [{ amount: cryptoAtoms, address: toAddress }], account_index: 0, subaddr_indices: [], @@ -142,103 +151,143 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) { unlock_time: 0, get_tx_hex: false, new_algorithm: false, - get_tx_metadata: false - })) + get_tx_metadata: false, + }), + ) .then(res => ({ fee: BN(res.fee_list[0]).abs(), - txid: res.tx_hash_list[0] + txid: res.tx_hash_list[0], })) .catch(err => handleError(err, 'sendCoins')) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info) { return checkCryptoCode(info.cryptoCode) .then(() => fetch('create_address', { account_index: 0 })) .then(res => res.address) .catch(err => handleError(err, 'newAddress')) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => refreshWallet()) .then(() => fetch('get_address_index', { address: toAddress })) - .then(addressRes => fetch('get_transfers', { in: true, pool: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] })) + .then(addressRes => + fetch('get_transfers', { + in: true, + pool: true, + account_index: addressRes.index.major, + subaddr_indices: [addressRes.index.minor], + }), + ) .then(transferRes => { - const confirmedToAddress = _.filter(it => it.address === toAddress, transferRes.in ?? []) - const pendingToAddress = _.filter(it => it.address === toAddress, transferRes.pool ?? []) - const confirmed = _.reduce((acc, value) => acc.plus(value.amount), BN(0), confirmedToAddress) - const pending = _.reduce((acc, value) => acc.plus(value.amount), BN(0), pendingToAddress) + const confirmedToAddress = _.filter( + it => it.address === toAddress, + transferRes.in ?? [], + ) + const pendingToAddress = _.filter( + it => it.address === toAddress, + transferRes.pool ?? [], + ) + const confirmed = _.reduce( + (acc, value) => acc.plus(value.amount), + BN(0), + confirmedToAddress, + ) + const pending = _.reduce( + (acc, value) => acc.plus(value.amount), + BN(0), + pendingToAddress, + ) - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } return { receivedCryptoAtoms: pending, status: 'notSeen' } }) .catch(err => handleError(err, 'getStatus')) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => refreshWallet()) - .then(() => Promise.all([ - fetch('get_balance', { account_index: 0, address_indices: [0] }), - fetch('create_address', { account_index: 0 }), - fetch('get_transfers', { pool: true, account_index: 0 }) - ])) + .then(() => + Promise.all([ + fetch('get_balance', { account_index: 0, address_indices: [0] }), + fetch('create_address', { account_index: 0 }), + fetch('get_transfers', { pool: true, account_index: 0 }), + ]), + ) .then(([balanceRes, addressRes, transferRes]) => { - const memPoolBalance = _.reduce((acc, value) => acc.plus(value.amount), BN(0), transferRes.pool) + const memPoolBalance = _.reduce( + (acc, value) => acc.plus(value.amount), + BN(0), + transferRes.pool, + ) return { - fundingPendingBalance: BN(balanceRes.balance).minus(balanceRes.unlocked_balance).plus(memPoolBalance), + fundingPendingBalance: BN(balanceRes.balance) + .minus(balanceRes.unlocked_balance) + .plus(memPoolBalance), fundingConfirmedBalance: BN(balanceRes.unlocked_balance), - fundingAddress: addressRes.address + fundingAddress: addressRes.address, } }) .catch(err => handleError(err, 'newFunding')) } -function cryptoNetwork (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => { - switch(parseInt(rpcConfig().port, 10)) { - case 18082: - return 'main' - case 28082: - return 'test' - case 38083: - return 'stage' - default: - return '' - } - }) -} - -function checkBlockchainStatus (cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => { - try { - const config = jsonRpc.parseConf(configPath) - - // Daemon uses a different connection of the wallet - const rpcConfig = { - username: config['rpc-login'].split(':')[0], - password: config['rpc-login'].split(':')[1], - port: cryptoRec.defaultPort - } - - return jsonRpc.fetchDigest(rpcConfig, 'get_info') - .then(res => !!res.synchronized ? 'ready' : 'syncing') - } catch (err) { - throw new Error('XMR daemon is currently not installed') +function cryptoNetwork(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => { + switch (parseInt(rpcConfig().port, 10)) { + case 18082: + return 'main' + case 28082: + return 'test' + case 38083: + return 'stage' + default: + return '' } }) } -function getTxHashesByAddress (cryptoCode, address) { +function checkBlockchainStatus(cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => { + try { + const config = jsonRpc.parseConf(configPath) + + // Daemon uses a different connection of the wallet + const rpcConfig = { + username: config['rpc-login'].split(':')[0], + password: config['rpc-login'].split(':')[1], + port: cryptoRec.defaultPort, + } + + return jsonRpc + .fetchDigest(rpcConfig, 'get_info') + .then(res => (res.synchronized ? 'ready' : 'syncing')) + } catch (err) { + throw new Error(`XMR daemon is currently not installed. ${err}`) + } + }) +} + +function getTxHashesByAddress(cryptoCode, address) { checkCryptoCode(cryptoCode) .then(() => refreshWallet()) .then(() => fetch('get_address_index', { address: address })) - .then(addressRes => fetch('get_transfers', { in: true, pool: true, pending: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] })) + .then(addressRes => + fetch('get_transfers', { + in: true, + pool: true, + pending: true, + account_index: addressRes.index.major, + subaddr_indices: [addressRes.index.minor], + }), + ) .then(_.map(({ txid }) => txid)) } @@ -250,5 +299,5 @@ module.exports = { newFunding, cryptoNetwork, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js b/packages/server/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js deleted file mode 100644 index afb0aa74..00000000 --- a/packages/server/lib/plugins/wallet/pazuz-wallet/pazuz-wallet.js +++ /dev/null @@ -1,98 +0,0 @@ -const https = require('https') -const BN = require('../../../bn') -const E = require('../../../error') -const _ = require('lodash/fp') - -const SUPPORTED_COINS = ['BTC'] - -const axios = require('axios').create({ - // TODO: get rejectUnauthorized true to work - baseURL: `${process.env.WALLET_URL}/api`, - httpsAgent: new https.Agent({ - rejectUnauthorized: false - }) -}) - -const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS) - ? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) - : Promise.resolve() - -function balance (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(() => { - return axios.post('/balance', { - cryptoCode, - config: settings.config, - operatorId - }) - }) - .then(({ data }) => { - if (data.error) throw new Error(JSON.stringify(data.error)) - return new BN(data.balance) - }) -} - -function sendCoins (account, tx, settings, operatorId) { - const { cryptoCode } = tx - return checkCryptoCode(cryptoCode) - .then(() => { - return axios.post('/sendCoins', { - tx, - config: settings.config, - operatorId - }) - }) - .then(({ data }) => { - if (data.error && data.error.errorCode === 'sc-001') throw new E.InsufficientFundsError() - else if (data.error) throw new Error(JSON.stringify(data.error)) - const fee = new BN(data.fee).decimalPlaces(0) - const txid = data.txid - return { txid, fee } - }) -} - -function newAddress (account, info, tx, settings, operatorId) { - return checkCryptoCode(info.cryptoCode) - .then(() => axios.post('/newAddress', { - info, - tx, - config: settings.config, - operatorId - })) - .then(({ data }) => { - if(data.error) throw new Error(JSON.stringify(data.error)) - return data.newAddress - }) -} - -function getStatus (account, tx, requested, settings, operatorId) { - return checkCryptoCode(tx.cryptoCode) - .then(() => axios.get(`/balance/${tx.toAddress}?cryptoCode=${tx.cryptoCode}`)) - .then(({ data }) => { - if (data.error) throw new Error(JSON.stringify(data.error)) - const confirmed = new BN(data.confirmedBalance) - const pending = new BN(data.pendingBalance) - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) -} - -function newFunding (account, cryptoCode, settings, operatorId) { - throw new E.NotImplementedError() -} - -function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) { - throw new E.NotImplementedError() -} - -module.exports = { - balance, - sendCoins, - newAddress, - newFunding, - getStatus, - sweep, - supportsHd: true, -} diff --git a/packages/server/lib/plugins/wallet/tron/base.js b/packages/server/lib/plugins/wallet/tron/base.js index c3a854f5..aa30325f 100644 --- a/packages/server/lib/plugins/wallet/tron/base.js +++ b/packages/server/lib/plugins/wallet/tron/base.js @@ -14,46 +14,54 @@ const SWEEP_QUEUE = new PQueue({ interval: 250, }) -function checkCryptoCode (cryptoCode) { +function checkCryptoCode(cryptoCode) { if (cryptoCode === 'TRX' || coins.utils.isTrc20Token(cryptoCode)) { return Promise.resolve(cryptoCode) } return Promise.reject(new Error('cryptoCode must be TRX')) } -function defaultWallet (account) { +function defaultWallet(account) { const mnemonic = account.mnemonic if (!mnemonic) throw new Error('No mnemonic seed!') - return TronWeb.fromMnemonic(mnemonic.replace(/[\r\n]/gm, ' ').trim(), `${DEFAULT_PREFIX_PATH}/0`) + return TronWeb.fromMnemonic( + mnemonic.replace(/[\r\n]/gm, ' ').trim(), + `${DEFAULT_PREFIX_PATH}/0`, + ) } -function paymentWallet (account, index) { +function paymentWallet(account, index) { const mnemonic = account.mnemonic if (!mnemonic) throw new Error('No mnemonic seed!') - return TronWeb.fromMnemonic(mnemonic.replace(/[\r\n]/gm, ' ').trim(), `${PAYMENT_PREFIX_PATH}/${index}`) + return TronWeb.fromMnemonic( + mnemonic.replace(/[\r\n]/gm, ' ').trim(), + `${PAYMENT_PREFIX_PATH}/${index}`, + ) } -function newAddress (account, info, tx, settings, operatorId) { +function newAddress(account, info) { const wallet = paymentWallet(account, info.hdIndex) return Promise.resolve(wallet.address) } -function defaultAddress (account) { +function defaultAddress(account) { return defaultWallet(account).address } -function balance (account, cryptoCode, settings, operatorId) { - return checkCryptoCode(cryptoCode) - .then(code => confirmedBalance(defaultAddress(account), code)) +function balance(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(code => + confirmedBalance(defaultAddress(account), code), + ) } const confirmedBalance = (address, cryptoCode) => _balance(address, cryptoCode) const _balance = async (address, cryptoCode) => { if (coins.utils.isTrc20Token(cryptoCode)) { - const contractAddress = coins.utils.getTrc20Token(cryptoCode).contractAddress + const contractAddress = + coins.utils.getTrc20Token(cryptoCode).contractAddress const { abi } = await tronWeb.trx.getContract(contractAddress) const contract = tronWeb.contract(abi.entrys, contractAddress) @@ -70,7 +78,12 @@ const sendCoins = async (account, tx) => { const isTrc20Token = coins.utils.isTrc20Token(cryptoCode) const txFunction = isTrc20Token ? generateTrc20Tx : generateTx - const rawTx = await txFunction(toAddress, defaultWallet(account), cryptoAtoms.toString(), cryptoCode) + const rawTx = await txFunction( + toAddress, + defaultWallet(account), + cryptoAtoms.toString(), + cryptoCode, + ) let response = null @@ -97,16 +110,26 @@ const generateTrc20Tx = async (toAddress, wallet, amount, cryptoCode) => { const functionSelector = 'transfer(address,uint256)' const parameters = [ { type: 'address', value: tronWeb.address.toHex(toAddress) }, - { type: 'uint256', value: amount } + { type: 'uint256', value: amount }, ] - const tx = await tronWeb.transactionBuilder.triggerSmartContract(contractAddress, functionSelector, {}, parameters, wallet.address) + const tx = await tronWeb.transactionBuilder.triggerSmartContract( + contractAddress, + functionSelector, + {}, + parameters, + wallet.address, + ) return tronWeb.trx.sign(tx.transaction, wallet.privateKey.slice(2)) } const generateTx = async (toAddress, wallet, amount) => { - const transaction = await tronWeb.transactionBuilder.sendTrx(toAddress, amount, wallet.address) + const transaction = await tronWeb.transactionBuilder.sendTrx( + toAddress, + amount, + wallet.address, + ) const privateKey = wallet.privateKey @@ -114,21 +137,19 @@ const generateTx = async (toAddress, wallet, amount) => { return tronWeb.trx.sign(transaction, privateKey.slice(2)) } -function newFunding (account, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(code => { - const fundingAddress = defaultAddress(account) +function newFunding(account, cryptoCode) { + return checkCryptoCode(cryptoCode).then(code => { + const fundingAddress = defaultAddress(account) - return confirmedBalance(fundingAddress, code) - .then((balance) => ({ - fundingPendingBalance: BN(0), - fundingConfirmedBalance: balance, - fundingAddress - })) - }) + return confirmedBalance(fundingAddress, code).then(balance => ({ + fundingPendingBalance: BN(0), + fundingConfirmedBalance: balance, + fundingAddress, + })) + }) } -function sweep (account, txId, cryptoCode, hdIndex) { +function sweep(account, txId, cryptoCode, hdIndex) { const wallet = paymentWallet(account, hdIndex) const fromAddress = wallet.address const isTrc20Token = coins.utils.isTrc20Token(cryptoCode) @@ -138,7 +159,12 @@ function sweep (account, txId, cryptoCode, hdIndex) { return SWEEP_QUEUE.add(async () => { const r = await confirmedBalance(fromAddress, cryptoCode) if (r.eq(0)) return - const signedTx = await txFunction(defaultAddress(account), wallet, r.toString(), cryptoCode) + const signedTx = await txFunction( + defaultAddress(account), + wallet, + r.toString(), + cryptoCode, + ) let response = null try { response = await tronWeb.trx.sendRawTransaction(signedTx) @@ -157,24 +183,28 @@ function connect(account) { const apiKey = account.apiKey tronWeb = new TronWeb({ fullHost: endpoint, - headers: { "TRON-PRO-API-KEY": apiKey }, - privateKey: '01' + headers: { 'TRON-PRO-API-KEY': apiKey }, + privateKey: '01', }) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(code => confirmedBalance(toAddress, code)) - .then((confirmed) => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - if (confirmed.gt(0)) return { receivedCryptoAtoms: confirmed, status: 'insufficientFunds' } + .then(confirmed => { + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gt(0)) + return { receivedCryptoAtoms: confirmed, status: 'insufficientFunds' } return { receivedCryptoAtoms: 0, status: 'notSeen' } }) } -function getTxHashesByAddress (cryptoCode, address) { - throw new Error(`Transactions hash retrieval is not implemented for this coin!`) +function getTxHashesByAddress() { + throw new Error( + `Transactions hash retrieval is not implemented for this coin!`, + ) } module.exports = { diff --git a/packages/server/lib/plugins/wallet/trongrid/trongrid.js b/packages/server/lib/plugins/wallet/trongrid/trongrid.js index 4d21a522..9244c0ca 100644 --- a/packages/server/lib/plugins/wallet/trongrid/trongrid.js +++ b/packages/server/lib/plugins/wallet/trongrid/trongrid.js @@ -3,7 +3,7 @@ const base = require('../tron/base') const NAME = 'trongrid' -function run (account) { +function run(account) { const endpoint = 'https://api.trongrid.io' base.connect({ ...account, endpoint }) diff --git a/packages/server/lib/plugins/wallet/zcashd/zcashd.js b/packages/server/lib/plugins/wallet/zcashd/zcashd.js index 1122bf18..6fb95508 100644 --- a/packages/server/lib/plugins/wallet/zcashd/zcashd.js +++ b/packages/server/lib/plugins/wallet/zcashd/zcashd.js @@ -1,5 +1,5 @@ const _ = require('lodash/fp') -const pRetry = require('p-retry') +const pRetry = require('p-retry') const jsonRpc = require('../../common/json-rpc') const { utils: coinUtils } = require('@lamassu/coins') @@ -12,11 +12,11 @@ const unitScale = cryptoRec.unitScale const rpcConfig = jsonRpc.rpcConfig(cryptoRec) -function fetch (method, params) { +function fetch(method, params) { return jsonRpc.fetch(rpcConfig, method, params) } -function errorHandle (e) { +function errorHandle(e) { const err = JSON.parse(e.message) switch (err.code) { case -6: @@ -26,134 +26,152 @@ function errorHandle (e) { } } -function checkCryptoCode (cryptoCode) { - if (cryptoCode !== 'ZEC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) +function checkCryptoCode(cryptoCode) { + if (cryptoCode !== 'ZEC') + return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode)) return Promise.resolve() } -function accountBalance (cryptoCode) { +function accountBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } -function accountUnconfirmedBalance (cryptoCode) { +function accountUnconfirmedBalance(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getwalletinfo')) - .then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0)) + .then(({ unconfirmed_balance: balance }) => + new BN(balance).shiftedBy(unitScale).decimalPlaces(0), + ) } // We want a balance that includes all spends (0 conf) but only deposits that // have at least 1 confirmation. getbalance does this for us automatically. -function balance (account, cryptoCode, settings, operatorId) { +function balance(account, cryptoCode) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins(account, tx) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) const checkSendStatus = function (opid) { return new Promise((resolve, reject) => { - fetch('z_getoperationstatus', [[opid]]) - .then(res => { - const status = _.get('status', res[0]) - switch (status) { - case 'success': - resolve(res[0]) - break - case 'failed': - throw new pRetry.AbortError(res[0].error) - case 'executing': - reject(new Error('operation still executing')) - break - } - }) + fetch('z_getoperationstatus', [[opid]]).then(res => { + const status = _.get('status', res[0]) + switch (status) { + case 'success': + resolve(res[0]) + break + case 'failed': + throw new pRetry.AbortError(res[0].error) + case 'executing': + reject(new Error('operation still executing')) + break + } + }) }) } - const checker = opid => pRetry(() => checkSendStatus(opid), { retries: 20, minTimeout: 300, factor: 1.05 }) + const checker = opid => + pRetry(() => checkSendStatus(opid), { + retries: 20, + minTimeout: 300, + factor: 1.05, + }) return checkCryptoCode(cryptoCode) - .then(() => fetch('z_sendmany', ['ANY_TADDR', [{ address: toAddress, amount: coins }], null, null, 'NoPrivacy'])) + .then(() => + fetch('z_sendmany', [ + 'ANY_TADDR', + [{ address: toAddress, amount: coins }], + null, + null, + 'NoPrivacy', + ]), + ) .then(checker) - .then((res) => { + .then(res => { return { fee: _.get('params.fee', res), - txid: _.get('result.txid', res) + txid: _.get('result.txid', res), } }) - .then((pickedObj) => { + .then(pickedObj => { return { fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0), - txid: pickedObj.txid + txid: pickedObj.txid, } }) .catch(errorHandle) } -function newAddress (account, info, tx, settings, operatorId) { - return checkCryptoCode(info.cryptoCode) - .then(() => fetch('getnewaddress')) +function newAddress(account, info) { + return checkCryptoCode(info.cryptoCode).then(() => fetch('getnewaddress')) } -function addressBalance (address, confs) { - return fetch('getreceivedbyaddress', [address, confs]) - .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0)) +function addressBalance(address, confs) { + return fetch('getreceivedbyaddress', [address, confs]).then(r => + new BN(r).shiftedBy(unitScale).decimalPlaces(0), + ) } -function confirmedBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 1)) +function confirmedBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 1)) } -function pendingBalance (address, cryptoCode) { - return checkCryptoCode(cryptoCode) - .then(() => addressBalance(address, 0)) +function pendingBalance(address, cryptoCode) { + return checkCryptoCode(cryptoCode).then(() => addressBalance(address, 0)) } -function getStatus (account, tx, requested, settings, operatorId) { +function getStatus(account, tx, requested) { const { toAddress, cryptoCode } = tx return checkCryptoCode(cryptoCode) .then(() => confirmedBalance(toAddress, cryptoCode)) .then(confirmed => { - if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' } + if (confirmed.gte(requested)) + return { receivedCryptoAtoms: confirmed, status: 'confirmed' } - return pendingBalance(toAddress, cryptoCode) - .then(pending => { - if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' } - if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } - return { receivedCryptoAtoms: pending, status: 'notSeen' } - }) + return pendingBalance(toAddress, cryptoCode).then(pending => { + if (pending.gte(requested)) + return { receivedCryptoAtoms: pending, status: 'authorized' } + if (pending.gt(0)) + return { receivedCryptoAtoms: pending, status: 'insufficientFunds' } + return { receivedCryptoAtoms: pending, status: 'notSeen' } + }) }) } -function newFunding (account, cryptoCode, settings, operatorId) { +function newFunding(account, cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => { const promises = [ accountUnconfirmedBalance(cryptoCode), accountBalance(cryptoCode), - newAddress(account, { cryptoCode }) + newAddress(account, { cryptoCode }), ] return Promise.all(promises) }) - .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ - fundingPendingBalance, - fundingConfirmedBalance, - fundingAddress - })) + .then( + ([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({ + fundingPendingBalance, + fundingConfirmedBalance, + fundingAddress, + }), + ) } -function checkBlockchainStatus (cryptoCode) { +function checkBlockchainStatus(cryptoCode) { return checkCryptoCode(cryptoCode) .then(() => fetch('getblockchaininfo')) - .then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing') + .then(res => (res['initial_block_download_complete'] ? 'ready' : 'syncing')) } -function getTxHashesByAddress (cryptoCode, address) { - checkCryptoCode(cryptoCode) - .then(() => fetch('getaddresstxids', [address])) +function getTxHashesByAddress(cryptoCode, address) { + checkCryptoCode(cryptoCode).then(() => fetch('getaddresstxids', [address])) } module.exports = { @@ -163,5 +181,5 @@ module.exports = { getStatus, newFunding, checkBlockchainStatus, - getTxHashesByAddress + getTxHashesByAddress, } diff --git a/packages/server/lib/plugins/zero-conf/blockcypher/blockcypher.js b/packages/server/lib/plugins/zero-conf/blockcypher/blockcypher.js index ef48e61f..7d19dba5 100644 --- a/packages/server/lib/plugins/zero-conf/blockcypher/blockcypher.js +++ b/packages/server/lib/plugins/zero-conf/blockcypher/blockcypher.js @@ -5,44 +5,64 @@ const _ = require('lodash/fp') const { fetchRBF } = require('../../wallet/bitcoind/bitcoind') module.exports = { authorize } -function highConfidence (confidence, txref, txRBF) { +function highConfidence(confidence, txref, txRBF) { if (txref.double_spend) return 0 if (txRBF) return 0 - if (txref.confirmations > 0 || txref.confidence * 100 >= confidence) return txref.value + if (txref.confirmations > 0 || txref.confidence * 100 >= confidence) + return txref.value return 0 } -function authorize (account, toAddress, cryptoAtoms, cryptoCode, isBitcoindAvailable) { - return Promise.resolve() - .then(() => { - if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode) +function authorize( + account, + toAddress, + cryptoAtoms, + cryptoCode, + isBitcoindAvailable, +) { + return Promise.resolve().then(() => { + if (cryptoCode !== 'BTC') + throw new Error('Unsupported crypto: ' + cryptoCode) - const query = qs.stringify({ - token: account.token, - includeConfidence: true - }) + const query = qs.stringify({ + token: account.token, + includeConfidence: true, + }) - const confidence = account.confidenceFactor - const isRBFEnabled = account.rbf - const url = `https://api.blockcypher.com/v1/btc/main/addrs/${toAddress}?${query}` + const confidence = account.confidenceFactor + const isRBFEnabled = account.rbf + const url = `https://api.blockcypher.com/v1/btc/main/addrs/${toAddress}?${query}` - return axios.get(url) - .then(r => { - const data = r.data - if (isBitcoindAvailable && isRBFEnabled && data.unconfirmed_txrefs) { - const promises = _.map(unconfirmedTxref => fetchRBF(unconfirmedTxref.tx_hash), data.unconfirmed_txrefs) - return Promise.all(promises) - .then(values => { - const unconfirmedTxsRBF = _.fromPairs(values) - const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref, unconfirmedTxsRBF[txref.tx_hash]), txrefs) - const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs) - return cryptoAtoms.lte(authorizedValue) - }) - } - - const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref), txrefs) - const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs) + return axios.get(url).then(r => { + const data = r.data + if (isBitcoindAvailable && isRBFEnabled && data.unconfirmed_txrefs) { + const promises = _.map( + unconfirmedTxref => fetchRBF(unconfirmedTxref.tx_hash), + data.unconfirmed_txrefs, + ) + return Promise.all(promises).then(values => { + const unconfirmedTxsRBF = _.fromPairs(values) + const sumTxRefs = txrefs => + _.sumBy( + txref => + highConfidence( + confidence, + txref, + unconfirmedTxsRBF[txref.tx_hash], + ), + txrefs, + ) + const authorizedValue = + sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs) return cryptoAtoms.lte(authorizedValue) }) + } + + const sumTxRefs = txrefs => + _.sumBy(txref => highConfidence(confidence, txref), txrefs) + const authorizedValue = + sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs) + return cryptoAtoms.lte(authorizedValue) }) + }) } diff --git a/packages/server/lib/plugins/zero-conf/mock-zero-conf/mock-zero-conf.js b/packages/server/lib/plugins/zero-conf/mock-zero-conf/mock-zero-conf.js index d2276170..a4c713a3 100644 --- a/packages/server/lib/plugins/zero-conf/mock-zero-conf/mock-zero-conf.js +++ b/packages/server/lib/plugins/zero-conf/mock-zero-conf/mock-zero-conf.js @@ -1,11 +1,11 @@ -module.exports = {authorize} +module.exports = { authorize } -function authorize (account, toAddress, cryptoAtoms, cryptoCode) { - return Promise.resolve() - .then(() => { - if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode) +function authorize(account, toAddress, cryptoAtoms, cryptoCode) { + return Promise.resolve().then(() => { + if (cryptoCode !== 'BTC') + throw new Error('Unsupported crypto: ' + cryptoCode) - const isAuthorized = false - return isAuthorized - }) + const isAuthorized = false + return isAuthorized + }) } diff --git a/packages/server/lib/poller.js b/packages/server/lib/poller.js index fefb8d16..227707a2 100644 --- a/packages/server/lib/poller.js +++ b/packages/server/lib/poller.js @@ -31,7 +31,6 @@ const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes const TICKER_RATES_INTERVAL = 59 * T.seconds const FAILED_SCANS_INTERVAL = 1 * T.day -const EXTERNAL_COMPLIANCE_INTERVAL = 1 * T.minutes const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds const PENDING_INTERVAL = 10 * T.seconds @@ -44,24 +43,24 @@ const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR const FAST_QUEUE = new Queue({ concurrent: 600, - interval: FAST_QUEUE_WAIT + interval: FAST_QUEUE_WAIT, }) const SLOW_QUEUE = new Queue({ concurrent: 10, - interval: SLOW_QUEUE_WAIT + interval: SLOW_QUEUE_WAIT, }) const QUEUE = { FAST: FAST_QUEUE, - SLOW: SLOW_QUEUE + SLOW: SLOW_QUEUE, } const cachedVariables = new NodeCache({ stdTTL: CACHE_ENTRY_TTL, checkperiod: CACHE_ENTRY_TTL, deleteOnExpire: false, - useClones: false // pass values by reference instead of cloning + useClones: false, // pass values by reference instead of cloning }) cachedVariables.on('expired', (key, val) => { @@ -72,61 +71,74 @@ cachedVariables.on('expired', (key, val) => { } }) -db.connect({ direct: true }).then(sco => { - sco.client.on('notification', () => { - return reload() - }) - return sco.none('LISTEN $1:name', 'reload') -}).catch(console.error) - -function reload () { - return settingsLoader.loadLatest() - .then(settings => { - const pi = plugins(settings) - cachedVariables.set('public', { settings, pi, isReloading: false }) - logger.debug(`Settings for schema 'public' reloaded in poller`) - return updateAndLoadSanctions() +db.connect({ direct: true }) + .then(sco => { + sco.client.on('notification', () => { + return reload() }) + return sco.none('LISTEN $1:name', 'reload') + }) + .catch(console.error) + +function reload() { + return settingsLoader.loadLatest().then(settings => { + const pi = plugins(settings) + cachedVariables.set('public', { settings, pi, isReloading: false }) + logger.debug(`Settings for schema 'public' reloaded in poller`) + return updateAndLoadSanctions() + }) } -function pi () { return cachedVariables.get('public').pi } -function settings () { return cachedVariables.get('public').settings } +function pi() { + return cachedVariables.get('public').pi +} +function settings() { + return cachedVariables.get('public').settings +} -function initialSanctionsDownload () { +function initialSanctionsDownload() { const structs = sanctions.getStructs() - const isEmptyStructs = _.isNil(structs) || _.flow(_.values, _.all(_.isEmpty))(structs) + const isEmptyStructs = + _.isNil(structs) || _.flow(_.values, _.all(_.isEmpty))(structs) if (!isEmptyStructs) return Promise.resolve() return updateAndLoadSanctions() } -function updateAndLoadSanctions () { +function updateAndLoadSanctions() { const triggers = configManager.getTriggers(settings().config) const hasSanctions = complianceTriggers.hasSanctions(triggers) if (!hasSanctions) return Promise.resolve() logger.info('Updating sanctions database...') - return sanctionsUpdater.update() + return sanctionsUpdater + .update() .then(sanctions.load) .then(() => logger.info('Sanctions database updated.')) } -function updateCoinAtmRadar () { - return pi().getRawRates() +function updateCoinAtmRadar() { + return pi() + .getRawRates() .then(rates => coinAtmRadar.update(rates, settings())) } const readdir = dirpath => - fs.readdir(dirpath, { withFileTypes: true }) + fs + .readdir(dirpath, { withFileTypes: true }) .then(_.map(entry => _.set('path', path.join(dirpath, entry.name), entry))) const readdirRec = rootPath => readdir(rootPath) - .then(entries => Promise.all( - entries.map(entry => entry.isDirectory() ? readdirRec(entry.path) : [entry]) - )) + .then(entries => + Promise.all( + entries.map(entry => + entry.isDirectory() ? readdirRec(entry.path) : [entry], + ), + ), + ) .then(_.flatten) const stat = path => fs.stat(path).then(_.set('path', path)) @@ -134,15 +146,19 @@ const pathComponents = p => path.normalize(p).split(path.sep) // @see lib/customers.js:updateIdCardData() const cleanOldFailedPDF417Scans = () => { - const matcher = (c, pat) => typeof pat === 'function' ? pat(c) : c === pat - const PDF417ScanPathPattern = _.concat( - pathComponents(OPERATOR_DATA_DIR), - ["id-operator", s => /* customerid*/ true, "idcarddata", fname => path.extname(fname) === 'jpg'] - ) + const matcher = (c, pat) => (typeof pat === 'function' ? pat(c) : c === pat) + const PDF417ScanPathPattern = _.concat(pathComponents(OPERATOR_DATA_DIR), [ + 'id-operator', + () => /* customerid*/ true, + 'idcarddata', + fname => path.extname(fname) === 'jpg', + ]) const isPDF417Scan = entry => { entry = pathComponents(entry.path) - return entry.length === PDF417ScanPathPattern.length - && _.isMatchWith(matcher, PDF417ScanPathPattern, pathComponents(entry.path)) + return ( + entry.length === PDF417ScanPathPattern.length && + _.isMatchWith(matcher, PDF417ScanPathPattern, pathComponents(entry.path)) + ) } let old = new Date() @@ -153,18 +169,20 @@ const cleanOldFailedPDF417Scans = () => { const isOld = filestat => filestat.mtimeMs < old return readdirRec(path.join(OPERATOR_DATA_DIR, 'id-operator')) - .then(entries => Promise.all( - entries - .filter(entry => entry.isFile() && isPDF417Scan(entry)) - .map(entry => stat(entry.path)) - )) - .then(filestats => Promise.all( - filestats - .filter(isOld) - .map(_.flow(_.get(['path']), fs.unlink)) - )) + .then(entries => + Promise.all( + entries + .filter(entry => entry.isFile() && isPDF417Scan(entry)) + .map(entry => stat(entry.path)), + ), + ) + .then(filestats => + Promise.all( + filestats.filter(isOld).map(_.flow(_.get(['path']), fs.unlink)), + ), + ) .catch(err => { - console.log("Error cleaning up failed PDF417 scans:", err) + console.log('Error cleaning up failed PDF417 scans:', err) }) } @@ -179,25 +197,30 @@ const cleanOldFailedQRScans = () => { } return readdirRec(path.join(OPERATOR_DATA_DIR, 'failedQRScans')) - .then(entries => Promise.all( - entries - .filter(entry => entry.isFile() && isOld(entry.path)) - .map(entry => fs.unlink(entry.path)) - )) + .then(entries => + Promise.all( + entries + .filter(entry => entry.isFile() && isOld(entry.path)) + .map(entry => fs.unlink(entry.path)), + ), + ) .catch(err => { - console.log("Error cleaning up failed QR scans:", err) + console.log('Error cleaning up failed QR scans:', err) }) } -function setup () { - return settingsLoader.loadLatest().then(settings => { - const pi = plugins(settings) - cachedVariables.set('public', { settings, pi, isReloading: false }) - return doPolling() - }).catch(console.error) +function setup() { + return settingsLoader + .loadLatest() + .then(settings => { + const pi = plugins(settings) + cachedVariables.set('public', { settings, pi, isReloading: false }) + return doPolling() + }) + .catch(console.error) } -function recursiveTimeout (func, timeout, ...vars) { +function recursiveTimeout(func, timeout, ...vars) { setTimeout(() => { let promise = null @@ -216,11 +239,11 @@ function recursiveTimeout (func, timeout, ...vars) { }, timeout) } -function addToQueue (func, interval, queue, ...vars) { +function addToQueue(func, interval, queue, ...vars) { recursiveTimeout(func, interval, ...vars) } -function doPolling () { +function doPolling() { pi().executeTrades() pi().clearOldLogs() cashOutTx.monitorLiveIncoming(settings()) @@ -232,20 +255,60 @@ function doPolling () { addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, QUEUE.FAST) addToQueue(pi().executeTrades, TRADE_INTERVAL, QUEUE.FAST) - addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, QUEUE.FAST, settings) - addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, QUEUE.FAST, settings) - addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, QUEUE.FAST, settings) + addToQueue( + cashOutTx.monitorLiveIncoming, + LIVE_INCOMING_TX_INTERVAL, + QUEUE.FAST, + settings, + ) + addToQueue( + cashOutTx.monitorStaleIncoming, + INCOMING_TX_INTERVAL, + QUEUE.FAST, + settings, + ) + addToQueue( + cashOutTx.monitorUnnotified, + UNNOTIFIED_INTERVAL, + QUEUE.FAST, + settings, + ) addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, QUEUE.FAST, settings) - addToQueue(processBatches, UNNOTIFIED_INTERVAL, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE) + addToQueue( + processBatches, + UNNOTIFIED_INTERVAL, + QUEUE.FAST, + settings, + TRANSACTION_BATCH_LIFECYCLE, + ) addToQueue(pi().sweepHd, SWEEP_HD_INTERVAL, QUEUE.FAST, settings) addToQueue(pi().clearOldLogs, LOGS_CLEAR_INTERVAL, QUEUE.SLOW) - addToQueue(notifier.checkNotification, CHECK_NOTIFICATION_INTERVAL, QUEUE.FAST, pi) - addToQueue(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL, QUEUE.SLOW) + addToQueue( + notifier.checkNotification, + CHECK_NOTIFICATION_INTERVAL, + QUEUE.FAST, + pi, + ) + addToQueue( + initialSanctionsDownload, + SANCTIONS_INITIAL_DOWNLOAD_INTERVAL, + QUEUE.SLOW, + ) addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, QUEUE.SLOW) addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, QUEUE.SLOW) - addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, QUEUE.SLOW, settings) + addToQueue( + pi().pruneMachinesHeartbeat, + PRUNE_MACHINES_HEARTBEAT, + QUEUE.SLOW, + settings, + ) addToQueue(cleanOldFailedQRScans, FAILED_SCANS_INTERVAL, QUEUE.SLOW, settings) - addToQueue(cleanOldFailedPDF417Scans, FAILED_SCANS_INTERVAL, QUEUE.SLOW, settings) + addToQueue( + cleanOldFailedPDF417Scans, + FAILED_SCANS_INTERVAL, + QUEUE.SLOW, + settings, + ) } module.exports = { setup, reload } diff --git a/packages/server/lib/postgresql_interface.js b/packages/server/lib/postgresql_interface.js index 014d52fa..65b5f1be 100644 --- a/packages/server/lib/postgresql_interface.js +++ b/packages/server/lib/postgresql_interface.js @@ -2,62 +2,76 @@ const _ = require('lodash/fp') const db = require('./db') const pgp = require('pg-promise')() -function getInsertQuery (tableName, fields) { +function getInsertQuery(tableName, fields) { // outputs string like: '$1, $2, $3...' with proper No of items - const placeholders = fields.map(function (_, i) { - return '$' + (i + 1) - }).join(', ') + const placeholders = fields + .map(function (_, i) { + return '$' + (i + 1) + }) + .join(', ') - const query = 'INSERT INTO ' + tableName + - ' (' + fields.join(', ') + ')' + + const query = + 'INSERT INTO ' + + tableName + + ' (' + + fields.join(', ') + + ')' + ' VALUES' + - ' (' + placeholders + ')' + ' (' + + placeholders + + ')' return query } -exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) { - const sql = 'INSERT INTO device_events (device_id, event_type, ' + +exports.recordDeviceEvent = function recordDeviceEvent(deviceId, event) { + const sql = + 'INSERT INTO device_events (device_id, event_type, ' + 'note, device_time) VALUES ($1, $2, $3, $4)' - const values = [deviceId, event.eventType, event.note, - event.deviceTime] + const values = [deviceId, event.eventType, event.note, event.deviceTime] return db.none(sql, values) } -exports.cassetteCounts = function cassetteCounts (deviceId) { - const sql = 'SELECT cassette1, cassette2, cassette3, cassette4, number_of_cassettes FROM devices ' + +exports.cassetteCounts = function cassetteCounts(deviceId) { + const sql = + 'SELECT cassette1, cassette2, cassette3, cassette4, number_of_cassettes FROM devices ' + 'WHERE device_id=$1' - return db.one(sql, [deviceId]) - .then(row => { - const counts = [] - _.forEach(it => { + return db.one(sql, [deviceId]).then(row => { + const counts = [] + _.forEach( + it => { counts.push(row[`cassette${it + 1}`]) - }, _.times(_.identity(), row.number_of_cassettes)) + }, + _.times(_.identity(), row.number_of_cassettes), + ) - return { numberOfCassettes: row.number_of_cassettes, counts } - }) + return { numberOfCassettes: row.number_of_cassettes, counts } + }) } -exports.recyclerCounts = function recyclerCounts (deviceId) { - const sql = 'SELECT recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, number_of_recyclers FROM devices ' + +exports.recyclerCounts = function recyclerCounts(deviceId) { + const sql = + 'SELECT recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, number_of_recyclers FROM devices ' + 'WHERE device_id=$1' - return db.one(sql, [deviceId]) - .then(row => { - const counts = [] - _.forEach(it => { + return db.one(sql, [deviceId]).then(row => { + const counts = [] + _.forEach( + it => { counts.push(row[`recycler${it + 1}`]) - }, _.times(_.identity(), row.number_of_recyclers)) + }, + _.times(_.identity(), row.number_of_recyclers), + ) - return { numberOfRecyclers: row.number_of_recyclers, counts } - }) + return { numberOfRecyclers: row.number_of_recyclers, counts } + }) } // Note: since we only prune on insert, we'll always have // last known state. -exports.machineEvent = function machineEvent (rec) { +exports.machineEvent = function machineEvent(rec) { const fields = ['id', 'device_id', 'event_type', 'note', 'device_time'] const sql = getInsertQuery('machine_events', fields) const values = [rec.id, rec.deviceId, rec.eventType, rec.note, rec.deviceTime] @@ -65,11 +79,10 @@ exports.machineEvent = function machineEvent (rec) { const deleteSql = `delete from machine_events where created < now() - interval '1 days'` - return db.none(sql, values) - .then(() => db.none(deleteSql)) + return db.none(sql, values).then(() => db.none(deleteSql)) } -exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) { +exports.machineEventsByIdBatch = function machineEventsByIdBatch(machineIds) { const formattedIds = _.map(pgp.as.text, machineIds).join(',') const sql = `SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events WHERE device_id IN ($1^) ORDER BY age ASC LIMIT 1` return db.any(sql, [formattedIds]).then(res => { @@ -79,8 +92,9 @@ exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) { }) } -exports.machineEvents = function machineEvents () { - const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events' +exports.machineEvents = function machineEvents() { + const sql = + 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events' return db.any(sql, []) } diff --git a/packages/server/lib/pp.js b/packages/server/lib/pp.js index 42ce480b..21407040 100644 --- a/packages/server/lib/pp.js +++ b/packages/server/lib/pp.js @@ -1,7 +1,7 @@ module.exports = function (label) { return function (o) { console.log(label) - console.log(require('util').inspect(o, {depth: null, colors: true})) + console.log(require('util').inspect(o, { depth: null, colors: true })) return o } } diff --git a/packages/server/lib/respond.js b/packages/server/lib/respond.js index 502e4ea3..3aa9a7e1 100644 --- a/packages/server/lib/respond.js +++ b/packages/server/lib/respond.js @@ -1,13 +1,18 @@ const _ = require('lodash/fp') const notifier = require('./notifier') -function respond (req, res, _body, _status) { +function respond(req, res, _body, _status) { const status = _status || 200 const body = _body || {} const customer = _.getOr({ sanctions: true }, ['customer'], body) // sanctions can be null for new customers so we can't use falsy checks if (customer.sanctions === false) { - notifier.notifyIfActive('compliance', 'sanctionsNotify', customer, req.body.phone) + notifier.notifyIfActive( + 'compliance', + 'sanctionsNotify', + customer, + req.body.phone, + ) } return res.status(status).json(body) } diff --git a/packages/server/lib/route-helpers.js b/packages/server/lib/route-helpers.js index ae4c173f..11e77d06 100644 --- a/packages/server/lib/route-helpers.js +++ b/packages/server/lib/route-helpers.js @@ -7,7 +7,7 @@ const BN = require('./bn') const TRANSACTION_EXPIRATION = T.day -function httpError (msg, code) { +function httpError(msg, code) { const err = new Error(msg) err.name = 'HTTPError' err.code = code || 500 @@ -15,18 +15,18 @@ function httpError (msg, code) { return err } -function stateChange (deviceId, deviceTime, rec) { +function stateChange(deviceId, deviceTime, rec) { const event = { id: rec.uuid, deviceId: deviceId, eventType: 'stateChange', note: JSON.stringify({ state: rec.state, isIdle: rec.isIdle }), - deviceTime: deviceTime + deviceTime: deviceTime, } return dbm.machineEvent(event) } -function toCashOutTx (row) { +function toCashOutTx(row) { if (!row) return null const keys = _.keys(row) @@ -45,18 +45,21 @@ function toCashOutTx (row) { return _.set('direction', 'cashOut', newObj) } -function fetchEmailOrPhoneTx (data, type) { +function fetchEmailOrPhoneTx(data, type) { const sql = `select * from cash_out_txs where ${type === 'email' ? 'email' : 'phone'}=$1 and dispense=$2 and (extract(epoch from (now() - created))) * 1000 < $3` const values = [data, false, TRANSACTION_EXPIRATION] - return db.any(sql, values) + return db + .any(sql, values) .then(_.map(toCashOutTx)) .then(txs => { const seenTxs = _.some(it => it.status !== 'notSeen', txs) - const confirmedTxs = txs.filter(tx => _.includes(tx.status, ['instant', 'confirmed'])) + const confirmedTxs = txs.filter(tx => + _.includes(tx.status, ['instant', 'confirmed']), + ) if (confirmedTxs.length > 0) { const reducer = (acc, val) => { return !acc || val.cryptoAtoms.gt(acc.cryptoAtoms) ? val : acc @@ -67,23 +70,26 @@ function fetchEmailOrPhoneTx (data, type) { return maxTx } - if (txs.length > 0 && !seenTxs) throw httpError('Transaction not seen', 411) - if (txs.length > 0 && seenTxs) throw httpError('Pending transactions', 412) + if (txs.length > 0 && !seenTxs) + throw httpError('Transaction not seen', 411) + if (txs.length > 0 && seenTxs) + throw httpError('Pending transactions', 412) throw httpError('No transactions', 404) }) } -function fetchEmailTx (email) { +function fetchEmailTx(email) { return fetchEmailOrPhoneTx(email, 'email') } -function fetchPhoneTx (phone) { +function fetchPhoneTx(phone) { return fetchEmailOrPhoneTx(phone, 'phone') } -function fetchStatusTx (txId, status) { +function fetchStatusTx(txId, status) { const sql = 'select * from cash_out_txs where id=$1' - return db.oneOrNone(sql, [txId]) + return db + .oneOrNone(sql, [txId]) .then(toCashOutTx) .then(tx => { if (!tx) throw httpError('No transaction', 404) @@ -97,5 +103,5 @@ module.exports = { fetchPhoneTx, fetchEmailTx, fetchStatusTx, - httpError + httpError, } diff --git a/packages/server/lib/routes.js b/packages/server/lib/routes.js index 110f9686..4c813c0f 100644 --- a/packages/server/lib/routes.js +++ b/packages/server/lib/routes.js @@ -47,7 +47,7 @@ const loadRoutes = async () => { '/customer', '/tx', '/verify_promo_code', - '/graphql' + '/graphql', ] // middleware setup @@ -59,11 +59,16 @@ const loadRoutes = async () => { morgan.token('bytesRead', (_req, res) => res.bytesRead) morgan.token('bytesWritten', (_req, res) => res.bytesWritten) - app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream })) + app.use( + morgan( + ':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', + { stream: logger.stream }, + ), + ) app.use('/robots.txt', (req, res) => { res.type('text/plain') - res.send("User-agent: *\nDisallow: /") + res.send('User-agent: *\nDisallow: /') }) app.get('/', (req, res) => { @@ -106,12 +111,13 @@ const loadRoutes = async () => { // rejecting poll is enough to render the machine "stuck" app.use(rejectIncompatibleMachines) await graphQLServer.start() - app.use('/graphql', + app.use( + '/graphql', express.json(), expressMiddleware(graphQLServer, { context, }), - ); + ) app.use(errorHandler) app.use((req, res) => { @@ -121,5 +127,4 @@ const loadRoutes = async () => { return app } - module.exports = { loadRoutes } diff --git a/packages/server/lib/routes/cashboxRoutes.js b/packages/server/lib/routes/cashboxRoutes.js index 27d29f14..905c76f3 100644 --- a/packages/server/lib/routes/cashboxRoutes.js +++ b/packages/server/lib/routes/cashboxRoutes.js @@ -4,16 +4,13 @@ const router = express.Router() const cashbox = require('../cashbox-batches') const notifier = require('../notifier') -const { getMachine, setMachine, getMachineName } = require('../machine-loader') +const { getMachine, getMachineName } = require('../machine-loader') const { loadLatestConfig } = require('../new-settings-loader') const { getCashInSettings } = require('../new-config-manager') const { AUTOMATIC } = require('../constants') const logger = require('../logger') - -function cashboxRemoval (req, res, next) { - const operatorId = res.locals.operatorId - +function cashboxRemoval(req, res, next) { notifier.cashboxNotify(req.deviceId).catch(logger.error) return Promise.all([getMachine(req.deviceId), loadLatestConfig()]) @@ -22,16 +19,23 @@ function cashboxRemoval (req, res, next) { if (cashInSettings.cashboxReset !== AUTOMATIC) { return Promise.all([ cashbox.getMachineUnbatchedBills(req.deviceId), - getMachineName(req.deviceId) + getMachineName(req.deviceId), ]) } - return cashbox.createCashboxBatch(req.deviceId, machine.cashbox) - .then(batch => Promise.all([ - cashbox.getBatchById(batch.id), - getMachineName(batch.device_id) - ])) + return cashbox + .createCashboxBatch(req.deviceId, machine.cashbox) + .then(batch => + Promise.all([ + cashbox.getBatchById(batch.id), + getMachineName(batch.device_id), + ]), + ) }) - .then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' })) + .then(([batch, machineName]) => + res + .status(200) + .send({ batch: _.merge(batch, { machineName }), status: 'OK' }), + ) .catch(next) } diff --git a/packages/server/lib/routes/customerRoutes.js b/packages/server/lib/routes/customerRoutes.js index 8b4ea330..7e387bf0 100644 --- a/packages/server/lib/routes/customerRoutes.js +++ b/packages/server/lib/routes/customerRoutes.js @@ -26,10 +26,14 @@ const loyalty = require('../loyalty') const logger = require('../logger') const externalCompliance = require('../compliance-external') -function updateCustomerCustomInfoRequest (customerId, patch) { - const promise = _.isNil(patch.data) ? - Promise.resolve(null) : - customInfoRequestQueries.setCustomerDataViaMachine(customerId, patch.infoRequestId, patch) +function updateCustomerCustomInfoRequest(customerId, patch) { + const promise = _.isNil(patch.data) + ? Promise.resolve(null) + : customInfoRequestQueries.setCustomerDataViaMachine( + customerId, + patch.infoRequestId, + patch, + ) return promise.then(() => customers.getById(customerId)) } @@ -37,21 +41,24 @@ const createPendingManualComplianceNotifs = (settings, customer, deviceId) => { const customInfoRequests = _.reduce( (reqs, req) => _.set(req.info_request_id, req, reqs), {}, - _.get(['customInfoRequestData'], customer) + _.get(['customInfoRequestData'], customer), ) const isPending = field => - uuid.validate(field) ? - _.get([field, 'override'], customInfoRequests) === 'automatic' : - customer[`${field}At`] - && (!customer[`${field}OverrideAt`] - || customer[`${field}OverrideAt`].getTime() < customer[`${field}At`].getTime()) + uuid.validate(field) + ? _.get([field, 'override'], customInfoRequests) === 'automatic' + : customer[`${field}At`] && + (!customer[`${field}OverrideAt`] || + customer[`${field}OverrideAt`].getTime() < + customer[`${field}At`].getTime()) const unnestCustomTriggers = triggersAutomation => { - const customTriggers = _.fromPairs(_.map(({ id, type }) => [id, type], triggersAutomation.custom)) + const customTriggers = _.fromPairs( + _.map(({ id, type }) => [id, type], triggersAutomation.custom), + ) return _.flow( _.unset('custom'), - _.mapKeys(k => k === 'facephoto' ? 'frontCamera' : k), + _.mapKeys(k => (k === 'facephoto' ? 'frontCamera' : k)), _.assign(customTriggers), )(triggersAutomation) } @@ -61,22 +68,31 @@ const createPendingManualComplianceNotifs = (settings, customer, deviceId) => { const hasManualAutomation = triggersAutomation => _.any(isManual, _.values(triggersAutomation)) - configManager.getTriggersAutomation(customInfoRequestQueries.getCustomInfoRequests(true), settings.config) + configManager + .getTriggersAutomation( + customInfoRequestQueries.getCustomInfoRequests(true), + settings.config, + ) .then(triggersAutomation => { triggersAutomation = unnestCustomTriggers(triggersAutomation) if (!hasManualAutomation(triggersAutomation)) return const pendingFields = _.filter( field => isManual(triggersAutomation[field]) && isPending(field), - _.keys(triggersAutomation) + _.keys(triggersAutomation), ) if (!_.isEmpty(pendingFields)) - notifier.complianceNotify(settings, customer, deviceId, 'PENDING_COMPLIANCE') + notifier.complianceNotify( + settings, + customer, + deviceId, + 'PENDING_COMPLIANCE', + ) }) } -function updateCustomer (req, res, next) { +function updateCustomer(req, res, next) { const id = req.params.id const patch = req.body const deviceId = req.deviceId @@ -91,8 +107,11 @@ function updateCustomer (req, res, next) { .catch(next) } - customers.getById(id) - .then(customer => !customer ? Promise.reject(httpError('Not Found', 404)) : {}) + customers + .getById(id) + .then(customer => + !customer ? Promise.reject(httpError('Not Found', 404)) : {}, + ) .then(_.merge(patch)) .then(newPatch => customers.updatePhotoCard(id, newPatch)) .then(newPatch => customers.updateFrontCamera(id, newPatch)) @@ -104,37 +123,44 @@ function updateCustomer (req, res, next) { .catch(next) } -function updateIdCardData (req, res, next) { +function updateIdCardData(req, res, next) { const id = req.params.id const patch = req.body - customers.getById(id) + customers + .getById(id) .then(customer => { - if (!customer) { throw httpError('Not Found', 404) } - return customers.updateIdCardData(patch, id) - .then(() => customer) + if (!customer) { + throw httpError('Not Found', 404) + } + return customers.updateIdCardData(patch, id).then(() => customer) }) .then(customer => respond(req, res, { customer })) .catch(next) } -function triggerSanctions (req, res, next) { +function triggerSanctions(req, res, next) { const id = req.params.id - customers.getById(id) + customers + .getById(id) .then(customer => { - if (!customer) { throw httpError('Not Found', 404) } - return compliance.validationPatch(req.deviceId, customer) + if (!customer) { + throw httpError('Not Found', 404) + } + return compliance + .validationPatch(req.deviceId, customer) .then(patch => customers.update(id, patch)) }) .then(customer => respond(req, res, { customer })) .catch(next) } -function triggerBlock (req, res, next) { +function triggerBlock(req, res, next) { const id = req.params.id const settings = req.settings - customers.update(id, { authorizedOverride: 'blocked' }) + customers + .update(id, { authorizedOverride: 'blocked' }) .then(customer => { notifier.complianceNotify(settings, customer, req.deviceId, 'BLOCKED') return respond(req, res, { customer }) @@ -142,27 +168,39 @@ function triggerBlock (req, res, next) { .catch(next) } -function triggerSuspend (req, res, next) { +function triggerSuspend(req, res, next) { const id = req.params.id const triggerId = req.body.triggerId const settings = req.settings const triggers = configManager.getTriggers(req.settings.config) - const getSuspendDays = _.compose(_.get('suspensionDays'), _.find(_.matches({ id: triggerId }))) + const getSuspendDays = _.compose( + _.get('suspensionDays'), + _.find(_.matches({ id: triggerId })), + ) - const days = _.includes(triggerId, ['no-ff-camera', 'id-card-photo-disabled']) ? 1 : getSuspendDays(triggers) + const days = _.includes(triggerId, ['no-ff-camera', 'id-card-photo-disabled']) + ? 1 + : getSuspendDays(triggers) const suspensionDuration = intervalToDuration({ start: 0, end: T.day * days }) - customers.update(id, { suspendedUntil: add(suspensionDuration, new Date()) }) + customers + .update(id, { suspendedUntil: add(suspensionDuration, new Date()) }) .then(customer => { - notifier.complianceNotify(settings, customer, req.deviceId, 'SUSPENDED', days) + notifier.complianceNotify( + settings, + customer, + req.deviceId, + 'SUSPENDED', + days, + ) return respond(req, res, { customer }) }) .catch(next) } -function updateTxCustomerPhoto (req, res, next) { +function updateTxCustomerPhoto(req, res, next) { const customerId = req.params.id const txId = req.params.txId const tcPhotoData = req.body.tcPhotoData @@ -171,63 +209,81 @@ function updateTxCustomerPhoto (req, res, next) { Promise.all([customers.getById(customerId), txs.getTx(txId, direction)]) .then(([customer, tx]) => { if (!customer || !tx) return - return customers.updateTxCustomerPhoto(tcPhotoData) - .then(newPatch => txs.updateTxCustomerPhoto(customerId, txId, direction, newPatch)) + return customers + .updateTxCustomerPhoto(tcPhotoData) + .then(newPatch => + txs.updateTxCustomerPhoto(customerId, txId, direction, newPatch), + ) }) .then(() => respond(req, res, {})) .catch(next) } -function buildSms (data, receiptOptions) { - return Promise.all([getTx(data.session, data.txClass), loadLatestConfig()]) - .then(([tx, config]) => { - return Promise.all([customers.getCustomerById(tx.customer_id), machineLoader.getMachine(tx.device_id, config)]) - .then(([customer, deviceConfig]) => { - const formattedTx = _.mapKeys(_.camelCase)(tx) - const localeConfig = configManager.getLocale(formattedTx.deviceId, config) - const timezone = localeConfig.timezone +function buildSms(data, receiptOptions) { + return Promise.all([ + getTx(data.session, data.txClass), + loadLatestConfig(), + ]).then(([tx, config]) => { + return Promise.all([ + customers.getCustomerById(tx.customer_id), + machineLoader.getMachine(tx.device_id, config), + ]).then(([customer, deviceConfig]) => { + const formattedTx = _.mapKeys(_.camelCase)(tx) + const localeConfig = configManager.getLocale(formattedTx.deviceId, config) + const timezone = localeConfig.timezone - const cashInCommission = new BN(1).plus(new BN(formattedTx.commissionPercentage)) + const cashInCommission = new BN(1).plus( + new BN(formattedTx.commissionPercentage), + ) - const rate = new BN(formattedTx.rawTickerPrice).multipliedBy(cashInCommission).decimalPlaces(2) - const date = utcToZonedTime(timezone, zonedTimeToUtc(process.env.TZ, new Date())) - const dateString = `${date.toISOString().replace('T', ' ').slice(0, 19)}` + const rate = new BN(formattedTx.rawTickerPrice) + .multipliedBy(cashInCommission) + .decimalPlaces(2) + const date = utcToZonedTime( + timezone, + zonedTimeToUtc(process.env.TZ, new Date()), + ) + const dateString = `${date.toISOString().replace('T', ' ').slice(0, 19)}` - const data = { - operatorInfo: configManager.getOperatorInfo(config), - location: deviceConfig.machineLocation, - customerName: customer.name, - customerPhone: customer.phone, - session: formattedTx.id, - time: dateString, - direction: formattedTx.txClass === 'cashIn' ? 'Cash-in' : 'Cash-out', - fiat: `${formattedTx.fiat.toString()} ${formattedTx.fiatCode}`, - crypto: `${sms.toCryptoUnits(BN(formattedTx.cryptoAtoms), formattedTx.cryptoCode)} ${formattedTx.cryptoCode}`, - rate: `1 ${formattedTx.cryptoCode} = ${rate} ${formattedTx.fiatCode}`, - address: formattedTx.toAddress, - txId: formattedTx.txHash - } + const data = { + operatorInfo: configManager.getOperatorInfo(config), + location: deviceConfig.machineLocation, + customerName: customer.name, + customerPhone: customer.phone, + session: formattedTx.id, + time: dateString, + direction: formattedTx.txClass === 'cashIn' ? 'Cash-in' : 'Cash-out', + fiat: `${formattedTx.fiat.toString()} ${formattedTx.fiatCode}`, + crypto: `${sms.toCryptoUnits(BN(formattedTx.cryptoAtoms), formattedTx.cryptoCode)} ${formattedTx.cryptoCode}`, + rate: `1 ${formattedTx.cryptoCode} = ${rate} ${formattedTx.fiatCode}`, + address: formattedTx.toAddress, + txId: formattedTx.txHash, + } - return sms.formatSmsReceipt(data, receiptOptions) - }) + return sms.formatSmsReceipt(data, receiptOptions) }) + }) } -function sendSmsReceipt (req, res, next) { - const receiptOptions = _.omit(['active', 'sms'], configManager.getReceipt(req.settings.config)) - buildSms(req.body.data, receiptOptions) - .then(smsRequest => { - sms.sendMessage(req.settings, smsRequest) - .then(() => respond(req, res, {})) - .catch(next) - }) +function sendSmsReceipt(req, res, next) { + const receiptOptions = _.omit( + ['active', 'sms'], + configManager.getReceipt(req.settings.config), + ) + buildSms(req.body.data, receiptOptions).then(smsRequest => { + sms + .sendMessage(req.settings, smsRequest) + .then(() => respond(req, res, {})) + .catch(next) + }) } -function getExternalComplianceLink (req, res, next) { +function getExternalComplianceLink(req, res, next) { const customerId = req.query.customer const triggerId = req.query.trigger const isRetry = req.query.isRetry - if (_.isNil(customerId) || _.isNil(triggerId)) return next(httpError('Not Found', 404)) + if (_.isNil(customerId) || _.isNil(triggerId)) + return next(httpError('Not Found', 404)) const settings = req.settings const triggers = configManager.getTriggers(settings.config) @@ -235,17 +291,27 @@ function getExternalComplianceLink (req, res, next) { const externalService = trigger.externalService if (isRetry) { - return externalCompliance.createLink(settings, externalService, customerId) + return externalCompliance + .createLink(settings, externalService, customerId) .then(url => respond(req, res, { url })) } - return externalCompliance.createApplicant(settings, externalService, customerId) - .then(applicant => customers.addExternalCompliance(customerId, externalService, applicant.id)) - .then(() => externalCompliance.createLink(settings, externalService, customerId)) + return externalCompliance + .createApplicant(settings, externalService, customerId) + .then(applicant => + customers.addExternalCompliance( + customerId, + externalService, + applicant.id, + ), + ) + .then(() => + externalCompliance.createLink(settings, externalService, customerId), + ) .then(url => respond(req, res, { url })) } -function addOrUpdateCustomer (customerData, deviceId, config, isEmailAuth) { +function addOrUpdateCustomer(customerData, deviceId, config, isEmailAuth) { const triggers = configManager.getTriggers(config) const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers) @@ -262,34 +328,42 @@ function addOrUpdateCustomer (customerData, deviceId, config, isEmailAuth) { .then(customer => customers.getById(customer.id)) .then(customer => { customers.updateLastAuthAttempt(customer.id, deviceId).catch(() => { - logger.info('failure updating last auth attempt for customer ', customer.id) + logger.info( + 'failure updating last auth attempt for customer ', + customer.id, + ) }) return customer }) .then(customer => { - return Tx.customerHistory(customer.id, maxDaysThreshold) - .then(result => { - customer.txHistory = result - return customer - }) + return Tx.customerHistory(customer.id, maxDaysThreshold).then(result => { + customer.txHistory = result + return customer + }) }) .then(customer => { - return loyalty.getCustomerActiveIndividualDiscount(customer.id) + return loyalty + .getCustomerActiveIndividualDiscount(customer.id) .then(discount => ({ ...customer, discount })) }) } -function getOrAddCustomerPhone (req, res, next) { +function getOrAddCustomerPhone(req, res, next) { const deviceId = req.deviceId const customerData = req.body const pi = plugins(req.settings, deviceId) const phone = req.body.phone - return pi.getPhoneCode(phone) + return pi + .getPhoneCode(phone) .then(code => { - return addOrUpdateCustomer(customerData, deviceId, req.settings.config, false) - .then(customer => respond(req, res, { code, customer })) + return addOrUpdateCustomer( + customerData, + deviceId, + req.settings.config, + false, + ).then(customer => respond(req, res, { code, customer })) }) .catch(err => { if (err.name === 'BadNumberError') throw httpError('Bad number', 401) @@ -298,17 +372,22 @@ function getOrAddCustomerPhone (req, res, next) { .catch(next) } -function getOrAddCustomerEmail (req, res, next) { +function getOrAddCustomerEmail(req, res, next) { const deviceId = req.deviceId const customerData = req.body const pi = plugins(req.settings, req.deviceId) const email = req.body.email - return pi.getEmailCode(email) + return pi + .getEmailCode(email) .then(code => { - return addOrUpdateCustomer(customerData, deviceId, req.settings.config, true) - .then(customer => respond(req, res, { code, customer })) + return addOrUpdateCustomer( + customerData, + deviceId, + req.settings.config, + true, + ).then(customer => respond(req, res, { code, customer })) }) .catch(err => { if (err.name === 'BadNumberError') throw httpError('Bad number', 401) diff --git a/packages/server/lib/routes/diagnosticsRoutes.js b/packages/server/lib/routes/diagnosticsRoutes.js index 41d9aa56..244b9a2b 100644 --- a/packages/server/lib/routes/diagnosticsRoutes.js +++ b/packages/server/lib/routes/diagnosticsRoutes.js @@ -3,7 +3,7 @@ const router = express.Router() const { updateDiagnostics } = require('../machine-loader') -function diagnostics (req, res, next) { +function diagnostics(req, res, next) { return updateDiagnostics(req.deviceId, req.body) .then(() => res.status(200).send({ status: 'OK' })) .catch(next) diff --git a/packages/server/lib/routes/failedQRScans.js b/packages/server/lib/routes/failedQRScans.js index e484ef36..cbf76da6 100644 --- a/packages/server/lib/routes/failedQRScans.js +++ b/packages/server/lib/routes/failedQRScans.js @@ -3,7 +3,7 @@ const router = express.Router() const { updateFailedQRScans } = require('../machine-loader') -function failedQRScans (req, res, next) { +function failedQRScans(req, res, next) { return updateFailedQRScans(req.deviceId, req.body) .then(() => res.status(200).send({ status: 'OK' })) .catch(next) diff --git a/packages/server/lib/routes/logsRoutes.js b/packages/server/lib/routes/logsRoutes.js index 59bef758..e482d014 100644 --- a/packages/server/lib/routes/logsRoutes.js +++ b/packages/server/lib/routes/logsRoutes.js @@ -6,15 +6,17 @@ const logs = require('../logs') const THROTTLE_LOGS_QUERY = 30 * 1000 -function getLastSeen (req, res, next) { +function getLastSeen(req, res, next) { const deviceId = req.deviceId const timestamp = Date.now() - const shouldTrigger = !state.canGetLastSeenMap[deviceId] || + const shouldTrigger = + !state.canGetLastSeenMap[deviceId] || timestamp - state.canGetLastSeenMap[deviceId] >= THROTTLE_LOGS_QUERY if (shouldTrigger) { state.canGetLastSeenMap[deviceId] = timestamp - return logs.getLastSeen(deviceId) + return logs + .getLastSeen(deviceId) .then(r => res.json(r)) .catch(next) } @@ -22,8 +24,9 @@ function getLastSeen (req, res, next) { return res.status(408).json({}) } -function updateLogs (req, res, next) { - return logs.update(req.deviceId, req.body.logs) +function updateLogs(req, res, next) { + return logs + .update(req.deviceId, req.body.logs) .then(status => res.json({ success: status })) .catch(next) } diff --git a/packages/server/lib/routes/pairingRoutes.js b/packages/server/lib/routes/pairingRoutes.js index f6a4016c..e0fb8ff0 100644 --- a/packages/server/lib/routes/pairingRoutes.js +++ b/packages/server/lib/routes/pairingRoutes.js @@ -6,14 +6,15 @@ const httpError = require('../route-helpers').httpError const pairing = require('../pairing') const populateDeviceId = require('../middlewares/populateDeviceId') -function pair (req, res, next) { +function pair(req, res, next) { const token = req.query.token const deviceId = req.deviceId const model = req.query.model const numOfCassettes = req.query.numOfCassettes const numOfRecyclers = req.query.numOfRecyclers - return pairing.pair(token, deviceId, model, numOfCassettes, numOfRecyclers) + return pairing + .pair(token, deviceId, model, numOfCassettes, numOfRecyclers) .then(isValid => { if (isValid) return res.json({ status: 'paired' }) throw httpError('Pairing failed') diff --git a/packages/server/lib/routes/performanceRoutes.js b/packages/server/lib/routes/performanceRoutes.js index d09330c2..1983a06b 100644 --- a/packages/server/lib/routes/performanceRoutes.js +++ b/packages/server/lib/routes/performanceRoutes.js @@ -1,15 +1,18 @@ const express = require('express') const router = express.Router() -const { updateNetworkHeartbeat, updateNetworkPerformance } = require('../machine-loader') +const { + updateNetworkHeartbeat, + updateNetworkPerformance, +} = require('../machine-loader') -function networkHeartbeat (req, res, next) { +function networkHeartbeat(req, res, next) { return updateNetworkHeartbeat(req.deviceId, req.body) .then(() => res.status(200).send({ status: 'OK' })) .catch(next) } -function networkPerformance (req, res, next) { +function networkPerformance(req, res, next) { return updateNetworkPerformance(req.deviceId, req.body) .then(() => res.status(200).send({ status: 'OK' })) .catch(next) diff --git a/packages/server/lib/routes/probeLnRoutes.js b/packages/server/lib/routes/probeLnRoutes.js index 62cf127e..babbbde3 100644 --- a/packages/server/lib/routes/probeLnRoutes.js +++ b/packages/server/lib/routes/probeLnRoutes.js @@ -4,17 +4,17 @@ const router = express.Router() const plugins = require('../plugins') const settingsLoader = require('../new-settings-loader') -function probe (req, res, next) { +function probe(req, res, next) { // TODO: why req.settings is undefined? - settingsLoader.loadLatest() - .then(settings => { - const pi = plugins(settings, req.deviceId) - return pi.probeLN('LN', req.body.address) - .then(r => res.status(200).send({ hardLimits: r })) - .catch(next) - }) + settingsLoader.loadLatest().then(settings => { + const pi = plugins(settings, req.deviceId) + return pi + .probeLN('LN', req.body.address) + .then(r => res.status(200).send({ hardLimits: r })) + .catch(next) + }) } router.get('/', probe) -module.exports = router \ No newline at end of file +module.exports = router diff --git a/packages/server/lib/routes/stateRoutes.js b/packages/server/lib/routes/stateRoutes.js index b25b62c0..c4f58e04 100644 --- a/packages/server/lib/routes/stateRoutes.js +++ b/packages/server/lib/routes/stateRoutes.js @@ -4,8 +4,9 @@ const router = express.Router() const helpers = require('../route-helpers') const respond = require('../respond') -function stateChange (req, res, next) { - helpers.stateChange(req.deviceId, req.deviceTime, req.body) +function stateChange(req, res, next) { + helpers + .stateChange(req.deviceId, req.deviceTime, req.body) .then(() => respond(req, res)) .catch(next) } diff --git a/packages/server/lib/routes/termsAndConditionsRoutes.js b/packages/server/lib/routes/termsAndConditionsRoutes.js index fef9f52d..c0ee85c5 100644 --- a/packages/server/lib/routes/termsAndConditionsRoutes.js +++ b/packages/server/lib/routes/termsAndConditionsRoutes.js @@ -6,21 +6,24 @@ const router = express.Router() const configManager = require('../new-config-manager') const settingsLoader = require('../new-settings-loader') -const createTerms = terms => (terms.active && terms.text) ? ({ - delay: terms.delay, - active: terms.active, - tcPhoto: terms.tcPhoto, - title: terms.title, - text: nmd(terms.text), - accept: terms.acceptButtonText, - cancel: terms.cancelButtonText -}) : null +const createTerms = terms => + terms.active && terms.text + ? { + delay: terms.delay, + active: terms.active, + tcPhoto: terms.tcPhoto, + title: terms.title, + text: nmd(terms.text), + accept: terms.acceptButtonText, + cancel: terms.cancelButtonText, + } + : null -function getTermsConditions (req, res, next) { - const deviceId = req.deviceId +function getTermsConditions(req, res, next) { const { config } = req.settings const terms = configManager.getTermsConditions(config) - return settingsLoader.fetchCurrentConfigVersion() + return settingsLoader + .fetchCurrentConfigVersion() .then(version => res.json({ terms: createTerms(terms), version })) .catch(next) } diff --git a/packages/server/lib/routes/txRoutes.js b/packages/server/lib/routes/txRoutes.js index f63755a0..7ddcca23 100644 --- a/packages/server/lib/routes/txRoutes.js +++ b/packages/server/lib/routes/txRoutes.js @@ -10,7 +10,7 @@ const logger = require('../logger') const plugins = require('../plugins') const Tx = require('../tx') -function postTx (req, res, next) { +function postTx(req, res, next) { const pi = plugins(req.settings, req.deviceId) return Tx.post(_.set('deviceId', req.deviceId, req.body), pi) @@ -38,17 +38,20 @@ function postTx (req, res, next) { logger.warn('Harmless DB conflict, the query will be retried.') return res.status(204).json({}) } - if (err instanceof E.StaleTxError) return res.status(409).json({ errorType: 'stale' }) - if (err instanceof E.RatchetError) return res.status(409).json({ errorType: 'ratchet' }) + if (err instanceof E.StaleTxError) + return res.status(409).json({ errorType: 'stale' }) + if (err instanceof E.RatchetError) + return res.status(409).json({ errorType: 'ratchet' }) throw err }) .catch(next) } -function getTx (req, res, next) { +function getTx(req, res, next) { if (req.query.status) { - return helpers.fetchStatusTx(req.params.id, req.query.status) + return helpers + .fetchStatusTx(req.params.id, req.query.status) .then(r => res.json(r)) .catch(next) } @@ -56,9 +59,10 @@ function getTx (req, res, next) { return next(httpError('Not Found', 404)) } -function getPhoneTx (req, res, next) { +function getPhoneTx(req, res, next) { if (req.query.phone) { - return helpers.fetchPhoneTx(req.query.phone) + return helpers + .fetchPhoneTx(req.query.phone) .then(r => res.json(r)) .catch(next) } @@ -66,9 +70,10 @@ function getPhoneTx (req, res, next) { return next(httpError('Not Found', 404)) } -function getEmailTx (req, res, next) { +function getEmailTx(req, res, next) { if (req.query.email) { - return helpers.fetchEmailTx(req.query.email) + return helpers + .fetchEmailTx(req.query.email) .then(r => res.json(r)) .catch(next) } diff --git a/packages/server/lib/routes/verifyPromoCodeRoutes.js b/packages/server/lib/routes/verifyPromoCodeRoutes.js index 6789a322..f8134723 100644 --- a/packages/server/lib/routes/verifyPromoCodeRoutes.js +++ b/packages/server/lib/routes/verifyPromoCodeRoutes.js @@ -7,26 +7,35 @@ const configManager = require('../new-config-manager') const loyalty = require('../loyalty') const respond = require('../respond') -function verifyPromoCode (req, res, next) { - loyalty.getPromoCode(req.body.codeInput) +function verifyPromoCode(req, res, next) { + loyalty + .getPromoCode(req.body.codeInput) .then(promoCode => { if (!promoCode) return next() const transaction = req.body.tx - const commissions = configManager.getCommissions(transaction.cryptoCode, req.deviceId, req.settings.config) + const commissions = configManager.getCommissions( + transaction.cryptoCode, + req.deviceId, + req.settings.config, + ) const tickerRate = new BN(transaction.rawTickerPrice) - const discount = commissionMath.getDiscountRate(promoCode.discount, commissions[transaction.direction]) + const discount = commissionMath.getDiscountRate( + promoCode.discount, + commissions[transaction.direction], + ) const rates = { [transaction.cryptoCode]: { - [transaction.direction]: (transaction.direction === 'cashIn') - ? tickerRate.times(discount).decimalPlaces(5) - : tickerRate.div(discount).decimalPlaces(5) - } + [transaction.direction]: + transaction.direction === 'cashIn' + ? tickerRate.times(discount).decimalPlaces(5) + : tickerRate.div(discount).decimalPlaces(5), + }, } respond(req, res, { promoCode: promoCode, - newRates: rates + newRates: rates, }) }) .catch(next) diff --git a/packages/server/lib/routes/verifyTxRoutes.js b/packages/server/lib/routes/verifyTxRoutes.js index 5c5cef3c..bec5a24b 100644 --- a/packages/server/lib/routes/verifyTxRoutes.js +++ b/packages/server/lib/routes/verifyTxRoutes.js @@ -4,7 +4,7 @@ const router = express.Router() const plugins = require('../plugins') const respond = require('../respond') -function verifyTx (req, res, next) { +function verifyTx(req, res, next) { const pi = plugins(req.settings, req.deviceId) pi.verifyTransaction(req.body) .then(idResult => respond(req, res, idResult)) diff --git a/packages/server/lib/routes/verifyUserRoutes.js b/packages/server/lib/routes/verifyUserRoutes.js index abf471b8..33d0db1b 100644 --- a/packages/server/lib/routes/verifyUserRoutes.js +++ b/packages/server/lib/routes/verifyUserRoutes.js @@ -4,7 +4,7 @@ const router = express.Router() const plugins = require('../plugins') const respond = require('../respond') -function verifyUser (req, res, next) { +function verifyUser(req, res, next) { const pi = plugins(req.settings, req.deviceId) pi.verifyUser(req.body) .then(idResult => respond(req, res, idResult)) diff --git a/packages/server/lib/sanctions.js b/packages/server/lib/sanctions.js index 33900835..7bb37d6d 100644 --- a/packages/server/lib/sanctions.js +++ b/packages/server/lib/sanctions.js @@ -6,20 +6,24 @@ const customers = require('./customers') const sanctionStatus = { loaded: false, - timestamp: null + timestamp: null, } const loadOrUpdateSanctions = () => { - if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) { + if ( + !sanctionStatus.loaded || + (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day) + ) { logger.info('No sanction lists loaded. Loading sanctions...') - return ofac.load() + return ofac + .load() .then(() => { logger.info('OFAC sanction list loaded!') sanctionStatus.loaded = true sanctionStatus.timestamp = Date.now() }) .catch(e => { - logger.error('Couldn\'t load OFAC sanction list!', e) + logger.error("Couldn't load OFAC sanction list!", e) }) } @@ -27,18 +31,28 @@ const loadOrUpdateSanctions = () => { } const checkByUser = (customerId, userToken) => { - return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)]) - .then(([, customer]) => { - const { firstName, lastName, dateOfBirth } = customer?.idCardData - const birthdate = _.replace(/-/g, '')(dateOfBirth) - const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) - const isOfacSanctioned = _.size(ofacMatches) > 0 - customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken) - - return { ofacSanctioned: isOfacSanctioned } + return Promise.all([ + loadOrUpdateSanctions(), + customers.getCustomerById(customerId), + ]).then(([, customer]) => { + const { firstName, lastName, dateOfBirth } = customer.idCardData + const birthdate = _.replace(/-/g, '')(dateOfBirth) + const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { + threshold: 0.85, + fullNameThreshold: 0.95, + debug: false, }) + const isOfacSanctioned = _.size(ofacMatches) > 0 + customers.updateCustomer( + customerId, + { sanctions: !isOfacSanctioned }, + userToken, + ) + + return { ofacSanctioned: isOfacSanctioned } + }) } module.exports = { - checkByUser + checkByUser, } diff --git a/packages/server/lib/session-manager.js b/packages/server/lib/session-manager.js index 3785c576..f337d96f 100644 --- a/packages/server/lib/session-manager.js +++ b/packages/server/lib/session-manager.js @@ -1,11 +1,11 @@ const db = require('./db') -function getSessions () { +function getSessions() { const sql = `SELECT * FROM user_sessions ORDER BY sess -> 'user' ->> 'username'` return db.any(sql) } -function getLastSessionPerUser () { +function getLastSessionPerUser() { const sql = `SELECT b.username, a.user_agent, a.ip_address, a.last_used, b.role FROM ( SELECT sess -> 'user' ->> 'username' AS username, sess ->> 'ua' AS user_agent, @@ -19,24 +19,31 @@ function getLastSessionPerUser () { return db.any(sql) } -function getSessionsByUsername (username) { +function getSessionsByUsername(username) { const sql = `SELECT * FROM user_sessions WHERE sess -> 'user' ->> 'username'=$1` return db.any(sql, [username]) } -function getSessionById (sessionID) { +function getSessionById(sessionID) { const sql = `SELECT * FROM user_sessions WHERE sid=$1` return db.any(sql, [sessionID]) } -function deleteSessionsByUsername (username) { +function deleteSessionsByUsername(username) { const sql = `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'username'=$1` return db.none(sql, [username]) } -function deleteSessionById (sessionID) { +function deleteSessionById(sessionID) { const sql = `DELETE FROM user_sessions WHERE sid=$1` return db.none(sql, [sessionID]) } -module.exports = { getSessions, getLastSessionPerUser, getSessionsByUsername, getSessionById, deleteSessionsByUsername, deleteSessionById } +module.exports = { + getSessions, + getLastSessionPerUser, + getSessionsByUsername, + getSessionById, + deleteSessionsByUsername, + deleteSessionById, +} diff --git a/packages/server/lib/sms-notices.js b/packages/server/lib/sms-notices.js index 182cf62f..64f67399 100644 --- a/packages/server/lib/sms-notices.js +++ b/packages/server/lib/sms-notices.js @@ -4,20 +4,31 @@ const db = require('./db') const getSMSNotices = () => { const sql = `SELECT * FROM sms_notices ORDER BY created` - return db.any(sql).then(res => _.map( - it => ({ - id: it.id, - event: _.camelCase(it.event), - message: it.message, - messageName: it.message_name, - enabled: it.enabled, - allowToggle: it.allow_toggle - }), res)) + return db.any(sql).then(res => + _.map( + it => ({ + id: it.id, + event: _.camelCase(it.event), + message: it.message, + messageName: it.message_name, + enabled: it.enabled, + allowToggle: it.allow_toggle, + }), + res, + ), + ) } const createSMSNotice = (event, messageName, message, enabled, allowToggle) => { const sql = `INSERT INTO sms_notices (id, event, message_name, message, enabled, allow_toggle) VALUES ($1, $2, $3, $4, $5, $6)` - return db.none(sql, [uuid.v4(), _.snakeCase(event), messageName, message, enabled, allowToggle]) + return db.none(sql, [ + uuid.v4(), + _.snakeCase(event), + messageName, + message, + enabled, + allowToggle, + ]) } const editSMSNotice = (id, event, message) => { @@ -52,5 +63,5 @@ module.exports = { deleteSMSNotice, getSMSNotice, enableSMSNotice, - disableSMSNotice + disableSMSNotice, } diff --git a/packages/server/lib/sms.js b/packages/server/lib/sms.js index 08d14635..62d16d9c 100644 --- a/packages/server/lib/sms.js +++ b/packages/server/lib/sms.js @@ -5,22 +5,25 @@ const _ = require('lodash/fp') const smsNotices = require('./sms-notices') const { RECEIPT } = require('./constants') -function getSms (event, phone, content) { - return smsNotices.getSMSNotice(event) - .then(msg => { - if (!_.isNil(msg)) { - var accMsg = msg.message - const contentKeys = _.keys(content) - const messageContent = _.reduce((acc, it) => _.replace(`#${it}`, content[it], acc), accMsg, contentKeys) - return { - toNumber: phone, - body: messageContent - } +function getSms(event, phone, content) { + return smsNotices.getSMSNotice(event).then(msg => { + if (!_.isNil(msg)) { + var accMsg = msg.message + const contentKeys = _.keys(content) + const messageContent = _.reduce( + (acc, it) => _.replace(`#${it}`, content[it], acc), + accMsg, + contentKeys, + ) + return { + toNumber: phone, + body: messageContent, } - }) + } + }) } -function getPlugin (settings) { +function getPlugin(settings) { const smsProvider = settings.config.notifications_thirdParty_sms const pluginCode = smsProvider ?? 'twilio' const plugin = ph.load(ph.SMS, pluginCode) @@ -29,12 +32,11 @@ function getPlugin (settings) { return { plugin, account } } -function sendMessage (settings, rec) { - return Promise.resolve() - .then(() => { - const { plugin, account } = getPlugin(settings) - return plugin.sendMessage(account, rec) - }) +function sendMessage(settings, rec) { + return Promise.resolve().then(() => { + const { plugin, account } = getPlugin(settings) + return plugin.sendMessage(account, rec) + }) } const toCryptoUnits = (cryptoAtoms, cryptoCode) => { @@ -42,7 +44,7 @@ const toCryptoUnits = (cryptoAtoms, cryptoCode) => { return cryptoAtoms.shiftedBy(-unitScale) } -function formatSmsReceipt (data, options) { +function formatSmsReceipt(data, options) { var message = `RECEIPT\n` if (data.operatorInfo) { message = message.concat(`Operator information:\n`) @@ -94,22 +96,26 @@ function formatSmsReceipt (data, options) { message = message.concat(`Address: ${data.address}\n`) } - const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC` + const timestamp = `${new Date().toISOString().substring(11, 19)} UTC` - const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { timestamp }) + const postReceiptSmsPromise = getSms(RECEIPT, data.customerPhone, { + timestamp, + }) - return Promise.all([smsNotices.getSMSNotice(RECEIPT), postReceiptSmsPromise]) - .then(([res, postReceiptSms]) => ({ - sms: { - toNumber: data.customerPhone, - body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message - } - })) + return Promise.all([ + smsNotices.getSMSNotice(RECEIPT), + postReceiptSmsPromise, + ]).then(([res, postReceiptSms]) => ({ + sms: { + toNumber: data.customerPhone, + body: res.enabled ? message.concat('\n\n', postReceiptSms.body) : message, + }, + })) } module.exports = { getSms, sendMessage, formatSmsReceipt, - toCryptoUnits + toCryptoUnits, } diff --git a/packages/server/lib/ticker.js b/packages/server/lib/ticker.js index 221235e3..f06ee3d4 100644 --- a/packages/server/lib/ticker.js +++ b/packages/server/lib/ticker.js @@ -17,47 +17,57 @@ const PEGGED_FIAT_CURRENCIES = { NAD: 'ZAR' } const getFallbackTicker = ticker => _.difference(['bitpay', 'kraken', 'bitstamp'], [ticker])[0] -const hasRatesOrReject = emsg => r => _.get(['rates'], r) ? - r : - Promise.reject(new Error(emsg)) +const hasRatesOrReject = emsg => r => + _.get(['rates'], r) ? r : Promise.reject(new Error(emsg)) const get1 = (market, fiatCode, cryptoCode, ticker, emsg) => buildTicker(fiatCode, cryptoCode, ticker) .then(hasRatesOrReject(emsg)) .then(({ rates }) => { - return lastRate[market] = { rates, timestamp: Date.now() } + return (lastRate[market] = { rates, timestamp: Date.now() }) }) -const _getRates = (settings, fiatCode, cryptoCode) => Promise.resolve() - .then(() => { - const ticker = configManager.getWalletSettings(cryptoCode, settings.config).ticker +const _getRates = (settings, fiatCode, cryptoCode) => + Promise.resolve().then(() => { + const ticker = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).ticker const market = [cryptoCode, fiatCode].join('-') const fallbackTicker = getFallbackTicker(ticker) - const emsg = fallbackTicker ? - "Failed to get rates with configured ticker, trying fallback" : - "Failed to get ticker rates" + const emsg = fallbackTicker + ? 'Failed to get rates with configured ticker, trying fallback' + : 'Failed to get ticker rates' return get1(market, fiatCode, cryptoCode, ticker, emsg) .catch(err => { logger.error(err) - return fallbackTicker ? - get1(market, fiatCode, cryptoCode, fallbackTicker, "Failed to get rates with fallback ticker") : - lastRate[market] + return fallbackTicker + ? get1( + market, + fiatCode, + cryptoCode, + fallbackTicker, + 'Failed to get rates with fallback ticker', + ) + : lastRate[market] }) - .then(hasRatesOrReject("Failed to get ticker rates")) + .then(hasRatesOrReject('Failed to get ticker rates')) }) -function buildTicker (fiatCode, cryptoCode, tickerName) { +function buildTicker(fiatCode, cryptoCode, tickerName) { fiatCode = _.defaultTo(fiatCode, _.get([fiatCode], PEGGED_FIAT_CURRENCIES)) cryptoCode = coinUtils.getEquivalentCode(cryptoCode) if (tickerName === 'bitpay') return bitpay.ticker(fiatCode, cryptoCode) - if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatCode, cryptoCode) + if (tickerName === 'mock-ticker') + return mockTicker.ticker(fiatCode, cryptoCode) return ccxt.ticker(fiatCode, cryptoCode, tickerName) } const getRates = mem(_getRates, { maxAge: FETCH_INTERVAL, - cacheKey: (settings, fiatCode, cryptoCode) => JSON.stringify([fiatCode, cryptoCode]) + cacheKey: (settings, fiatCode, cryptoCode) => + JSON.stringify([fiatCode, cryptoCode]), }) module.exports = { getRates } diff --git a/packages/server/lib/time.js b/packages/server/lib/time.js index 37cf1bdb..0061bca9 100644 --- a/packages/server/lib/time.js +++ b/packages/server/lib/time.js @@ -23,5 +23,5 @@ module.exports = { weeks, week, years, - year + year, } diff --git a/packages/server/lib/tx-batching-processing.js b/packages/server/lib/tx-batching-processing.js index 94f4791d..a230b4ae 100644 --- a/packages/server/lib/tx-batching-processing.js +++ b/packages/server/lib/tx-batching-processing.js @@ -3,28 +3,28 @@ const _ = require('lodash/fp') const txBatching = require('./tx-batching') const wallet = require('./wallet') -function submitBatch (settings, batch) { - txBatching.getBatchTransactions(batch) - .then(txs => { - if (_.isEmpty(txs)) return Promise.resolve() - return wallet.sendCoinsBatch(settings, txs, batch.crypto_code) - .then(res => txBatching.confirmSentBatch(batch, res)) - .catch(err => txBatching.setErroredBatch(batch, err.message)) - }) +function submitBatch(settings, batch) { + txBatching.getBatchTransactions(batch).then(txs => { + if (_.isEmpty(txs)) return Promise.resolve() + return wallet + .sendCoinsBatch(settings, txs, batch.crypto_code) + .then(res => txBatching.confirmSentBatch(batch, res)) + .catch(err => txBatching.setErroredBatch(batch, err.message)) + }) } -function processBatches (settings, lifecycle) { - return txBatching.getBatchesByStatus(['open']) - .then(batches => { - _.each(batch => { - const elapsedMS = batch.time_elapsed * 1000 +function processBatches(settings, lifecycle) { + return txBatching.getBatchesByStatus(['open']).then(batches => { + _.each(batch => { + const elapsedMS = batch.time_elapsed * 1000 - if (elapsedMS >= lifecycle) { - return txBatching.closeTransactionBatch(batch) - .then(() => submitBatch(settings, batch)) - } - }, batches) - }) + if (elapsedMS >= lifecycle) { + return txBatching + .closeTransactionBatch(batch) + .then(() => submitBatch(settings, batch)) + } + }, batches) + }) } module.exports = processBatches diff --git a/packages/server/lib/tx-batching.js b/packages/server/lib/tx-batching.js index 2b7e793e..e6382bcc 100644 --- a/packages/server/lib/tx-batching.js +++ b/packages/server/lib/tx-batching.js @@ -5,62 +5,72 @@ const uuid = require('uuid') const BN = require('./bn') const db = require('./db') -function addTransactionToBatch (tx) { +function addTransactionToBatch(tx) { const sql = `SELECT * FROM transaction_batches WHERE crypto_code=$1 AND status='open' ORDER BY created_at DESC LIMIT 1` const sql2 = `UPDATE cash_in_txs SET batch_id=$1 WHERE id=$2` - return db.oneOrNone(sql, [tx.cryptoCode]) - .then(batch => { - if (_.isNil(batch)) { - return db.tx(t => { - const newBatchId = uuid.v4() - const q1 = t.none(`INSERT INTO transaction_batches (id, crypto_code) VALUES ($1, $2)`, [newBatchId, tx.cryptoCode]) - const q2 = t.none(sql2, [newBatchId, tx.id]) + return db.oneOrNone(sql, [tx.cryptoCode]).then(batch => { + if (_.isNil(batch)) { + return db.tx(t => { + const newBatchId = uuid.v4() + const q1 = t.none( + `INSERT INTO transaction_batches (id, crypto_code) VALUES ($1, $2)`, + [newBatchId, tx.cryptoCode], + ) + const q2 = t.none(sql2, [newBatchId, tx.id]) - return t.batch([q1, q2]) - }) - } - return db.none(sql2, [batch.id, tx.id]) - }) + return t.batch([q1, q2]) + }) + } + return db.none(sql2, [batch.id, tx.id]) + }) } -function closeTransactionBatch (batch) { +function closeTransactionBatch(batch) { const sql = `UPDATE transaction_batches SET status='ready', closed_at=now() WHERE id=$1` return db.none(sql, [batch.id]) } -function confirmSentBatch (batch, tx) { +function confirmSentBatch(batch, tx) { return db.tx(t => { - const q1 = t.none(`UPDATE transaction_batches SET status='sent', error_message=NULL WHERE id=$1`, [batch.id]) - const q2 = t.none(`UPDATE cash_in_txs SET tx_hash=$1, fee=$2, send=true, send_confirmed=true, send_time=now(), send_pending=false, error=NULL, error_code=NULL WHERE batch_id=$3`, [tx.txid, tx.fee.toString(), batch.id]) + const q1 = t.none( + `UPDATE transaction_batches SET status='sent', error_message=NULL WHERE id=$1`, + [batch.id], + ) + const q2 = t.none( + `UPDATE cash_in_txs SET tx_hash=$1, fee=$2, send=true, send_confirmed=true, send_time=now(), send_pending=false, error=NULL, error_code=NULL WHERE batch_id=$3`, + [tx.txid, tx.fee.toString(), batch.id], + ) return t.batch([q1, q2]) }) } -function setErroredBatch (batch, errorMsg) { +function setErroredBatch(batch, errorMsg) { const sql = `UPDATE transaction_batches SET status='failed', error_message=$1 WHERE id=$2` return db.none(sql, [errorMsg, batch.id]) } -function getBatchTransactions (batch) { +function getBatchTransactions(batch) { const sql = `SELECT * FROM cash_in_txs WHERE batch_id=$1` - return db.manyOrNone(sql, [batch.id]) + return db + .manyOrNone(sql, [batch.id]) .then(res => _.map(_.mapKeys(_.camelCase), res)) } -function getBatchesByStatus (statuses) { +function getBatchesByStatus(statuses) { const sql = `SELECT *, EXTRACT(EPOCH FROM (now() - created_at)) as time_elapsed FROM transaction_batches WHERE status in ($1^)` return db.manyOrNone(sql, [_.map(pgp.as.text, statuses).join(',')]) } -function getOpenBatchCryptoValue (cryptoCode) { +function getOpenBatchCryptoValue(cryptoCode) { const sql = `SELECT * FROM transaction_batches WHERE crypto_code=$1 AND status='open' ORDER BY created_at DESC LIMIT 1` - return db.oneOrNone(sql, [cryptoCode]) + return db + .oneOrNone(sql, [cryptoCode]) .then(batch => { if (_.isNil(batch)) return Promise.resolve([]) return db.any(`SELECT * FROM cash_in_txs WHERE batch_id=$1`, [batch.id]) @@ -75,5 +85,5 @@ module.exports = { setErroredBatch, getBatchTransactions, getBatchesByStatus, - getOpenBatchCryptoValue + getOpenBatchCryptoValue, } diff --git a/packages/server/lib/tx.js b/packages/server/lib/tx.js index d43c2818..5b0cf010 100644 --- a/packages/server/lib/tx.js +++ b/packages/server/lib/tx.js @@ -9,41 +9,41 @@ const T = require('./time') // E.g.: 1853.013808 * 1000 = 1866149.494 const REDEEMABLE_AGE = T.day / 1000 -function process (tx, pi) { - const mtx = massage(tx, pi) +function process(tx, pi) { + const mtx = massage(tx) if (mtx.direction === 'cashIn') return CashInTx.post(mtx, pi) if (mtx.direction === 'cashOut') return CashOutTx.post(mtx, pi) return Promise.reject(new Error('No such tx direction: ' + mtx.direction)) } -function post (tx, pi) { - return process(tx, pi) - .then(_.set('dirty', false)) +function post(tx, pi) { + return process(tx, pi).then(_.set('dirty', false)) } -function massage (tx, pi) { +function massage(tx) { const isDateField = r => r === 'created' || _.endsWith('_time', r) - const transformDate = (v, k) => isDateField(k) ? new Date(v) : v - const mapValuesWithKey = _.mapValues.convert({'cap': false}) + const transformDate = (v, k) => (isDateField(k) ? new Date(v) : v) + const mapValuesWithKey = _.mapValues.convert({ cap: false }) const transformDates = r => mapValuesWithKey(transformDate, r) const mapBN = r => { - const update = r.direction === 'cashIn' - ? { - cryptoAtoms: new BN(r.cryptoAtoms), - fiat: new BN(r.fiat), - cashInFee: new BN(r.cashInFee), - commissionPercentage: new BN(r.commissionPercentage), - rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null, - minimumTx: new BN(r.minimumTx) - } - : { - cryptoAtoms: new BN(r.cryptoAtoms), - fiat: new BN(r.fiat), - fixedFee: r.cashOutFee ? new BN(r.cashOutFee) : null, - rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null, - commissionPercentage: new BN(r.commissionPercentage) - } + const update = + r.direction === 'cashIn' + ? { + cryptoAtoms: new BN(r.cryptoAtoms), + fiat: new BN(r.fiat), + cashInFee: new BN(r.cashInFee), + commissionPercentage: new BN(r.commissionPercentage), + rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null, + minimumTx: new BN(r.minimumTx), + } + : { + cryptoAtoms: new BN(r.cryptoAtoms), + fiat: new BN(r.fiat), + fixedFee: r.cashOutFee ? new BN(r.cashOutFee) : null, + rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null, + commissionPercentage: new BN(r.commissionPercentage), + } return _.assign(r, update) } @@ -52,26 +52,29 @@ function massage (tx, pi) { transformDates, mapBN, _.unset('dirty'), - _.unset('cashOutFee') + _.unset('cashOutFee'), ) return mapper(tx) } -function cancel (txId) { +function cancel(txId) { const promises = [ - CashInTx.cancel(txId).then(() => true).catch(() => false), - CashOutTx.cancel(txId).then(() => true).catch(() => false) + CashInTx.cancel(txId) + .then(() => true) + .catch(() => false), + CashOutTx.cancel(txId) + .then(() => true) + .catch(() => false), ] - return Promise.all(promises) - .then(r => { - if (_.some(r)) return - throw new Error('No such transaction') - }) + return Promise.all(promises).then(r => { + if (_.some(r)) return + throw new Error('No such transaction') + }) } -function customerHistory (customerId, thresholdDays) { +function customerHistory(customerId, thresholdDays) { const sql = `SELECT ch.id, ch.created, ch.fiat, ch.direction FROM ( SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction, ((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired diff --git a/packages/server/lib/users.js b/packages/server/lib/users.js index 3eaa1b74..08677719 100644 --- a/packages/server/lib/users.js +++ b/packages/server/lib/users.js @@ -17,7 +17,7 @@ const db = require('./db') * * @returns {user object} User object (containing name) */ -function get (token) { +function get(token) { const sql = 'SELECT * FROM user_tokens WHERE token=$1' return db.oneOrNone(sql, [token]) } @@ -32,30 +32,31 @@ function get (token) { * * @returns {array} Array of users found */ -function getByIds (ids) { +function getByIds(ids) { const sql = `SELECT * FROM users WHERE id IN ($1^)` const idList = _.map(pgp.as.text, ids).join(',') return db.any(sql, [idList]) } -function getUserById (id) { +function getUserById(id) { const sql = `SELECT * FROM users WHERE id=$1` return db.oneOrNone(sql, [id]) } -function getUserByUsername (username) { +function getUserByUsername(username) { const sql = `SELECT * FROM users WHERE username=$1` return db.oneOrNone(sql, [username]) } -function getUsers () { +function getUsers() { const sql = `SELECT id, username, role, enabled, last_accessed, last_accessed_from, last_accessed_address FROM users ORDER BY username` return db.any(sql) } -function verifyAndUpdateUser (id, ua, ip) { +function verifyAndUpdateUser(id, ua, ip) { const sql = `SELECT id, username, role, enabled FROM users WHERE id=$1 limit 1` - return db.oneOrNone(sql, [id]) + return db + .oneOrNone(sql, [id]) .then(user => { if (!user) return null @@ -65,54 +66,79 @@ function verifyAndUpdateUser (id, ua, ip) { .then(user => user) } -function saveTemp2FASecret (id, secret) { +function saveTemp2FASecret(id, secret) { const sql = 'UPDATE users SET temp_twofa_code=$1 WHERE id=$2' return db.none(sql, [secret, id]) } -function save2FASecret (id, secret) { +function save2FASecret(id, secret) { return db.tx(t => { - const q1 = t.none('UPDATE users SET twofa_code=$1, temp_twofa_code=NULL WHERE id=$2', [secret, id]) - const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) + const q1 = t.none( + 'UPDATE users SET twofa_code=$1, temp_twofa_code=NULL WHERE id=$2', + [secret, id], + ) + const q2 = t.none( + `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, + [id], + ) return t.batch([q1, q2]) }) } -function validateAuthToken (token, type) { +function validateAuthToken(token, type) { const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens WHERE token=$1 AND type=$2` - return db.one(sql, [token, type]) + return db + .one(sql, [token, type]) .then(res => ({ userID: res.user_id, success: res.success })) } -function reset2FASecret (token, id, secret) { +function reset2FASecret(token, id, secret) { return validateAuthToken(token, 'reset_twofa').then(res => { if (!res.success) throw new Error('Failed to verify 2FA reset token') return db.tx(t => { - const q1 = t.none('UPDATE users SET twofa_code=$1, temp_twofa_code=NULL WHERE id=$2', [secret, id]) - const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) - const q3 = t.none(`DELETE FROM auth_tokens WHERE token=$1 and type='reset_twofa'`, [token]) + const q1 = t.none( + 'UPDATE users SET twofa_code=$1, temp_twofa_code=NULL WHERE id=$2', + [secret, id], + ) + const q2 = t.none( + `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, + [id], + ) + const q3 = t.none( + `DELETE FROM auth_tokens WHERE token=$1 and type='reset_twofa'`, + [token], + ) return t.batch([q1, q2, q3]) }) }) } -function updatePassword (token, id, password) { +function updatePassword(token, id, password) { return validateAuthToken(token, 'reset_password').then(res => { if (!res.success) throw new Error('Failed to verify password reset token') return argon2.hash(password).then(function (hash) { return db.tx(t => { - const q1 = t.none(`UPDATE users SET password=$1 WHERE id=$2`, [hash, id]) - const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) - const q3 = t.none(`DELETE FROM auth_tokens WHERE token=$1 and type='reset_password'`, [token]) + const q1 = t.none(`UPDATE users SET password=$1 WHERE id=$2`, [ + hash, + id, + ]) + const q2 = t.none( + `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, + [id], + ) + const q3 = t.none( + `DELETE FROM auth_tokens WHERE token=$1 and type='reset_password'`, + [token], + ) return t.batch([q1, q2, q3]) }) }) }) } -function createUserRegistrationToken (username, role) { +function createUserRegistrationToken(username, role) { const token = crypto.randomBytes(32).toString('hex') const sql = `INSERT INTO user_register_tokens (token, username, role) VALUES ($1, $2, $3) ON CONFLICT (username) DO UPDATE SET token=$1, expire=now() + interval '${constants.REGISTRATION_TOKEN_EXPIRATION_TIME}' RETURNING *` @@ -120,40 +146,51 @@ function createUserRegistrationToken (username, role) { return db.one(sql, [token, username, role]) } -function validateUserRegistrationToken (token) { +function validateUserRegistrationToken(token) { const sql = `SELECT username, role, now() < expire AS success FROM user_register_tokens WHERE token=$1` - return db.one(sql, [token]) - .then(res => ({ username: res.username, role: res.role, success: res.success })) + return db.one(sql, [token]).then(res => ({ + username: res.username, + role: res.role, + success: res.success, + })) } -function register (token, username, password, role) { +function register(token, username, password, role) { return validateUserRegistrationToken(token).then(res => { if (!res.success) throw new Error('Failed to verify registration token') return argon2.hash(password).then(hash => { return db.tx(t => { - const q1 = t.none(`INSERT INTO users (id, username, password, role) VALUES ($1, $2, $3, $4)`, [uuid.v4(), username, hash, role]) - const q2 = t.none(`DELETE FROM user_register_tokens WHERE token=$1`, [token]) + const q1 = t.none( + `INSERT INTO users (id, username, password, role) VALUES ($1, $2, $3, $4)`, + [uuid.v4(), username, hash, role], + ) + const q2 = t.none(`DELETE FROM user_register_tokens WHERE token=$1`, [ + token, + ]) return t.batch([q1, q2]) }) }) }) } -function changeUserRole (id, newRole) { +function changeUserRole(id, newRole) { const sql = `UPDATE users SET role=$1 WHERE id=$2` return db.none(sql, [newRole, id]) } -function enableUser (id) { +function enableUser(id) { const sql = `UPDATE users SET enabled=true WHERE id=$1` return db.none(sql, [id]) } -function disableUser (id) { +function disableUser(id) { return db.tx(t => { const q1 = t.none(`UPDATE users SET enabled=false WHERE id=$1`, [id]) - const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) + const q2 = t.none( + `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, + [id], + ) return t.batch([q1, q2]) }) } @@ -175,5 +212,5 @@ module.exports = { register, changeUserRole, enableUser, - disableUser + disableUser, } diff --git a/packages/server/lib/utils.js b/packages/server/lib/utils.js index a6f7dc45..7f3c01ef 100644 --- a/packages/server/lib/utils.js +++ b/packages/server/lib/utils.js @@ -3,7 +3,8 @@ const _ = require('lodash') const camelize = obj => _.transform(obj, (acc, value, key, target) => { const camelKey = _.isArray(target) ? key : _.camelCase(key.toString()) - acc[camelKey] = _.isObject(value) && !(value instanceof Date) ? camelize(value) : value + acc[camelKey] = + _.isObject(value) && !(value instanceof Date) ? camelize(value) : value }) module.exports = camelize diff --git a/packages/server/lib/wallet-scoring.js b/packages/server/lib/wallet-scoring.js index 9b426faf..4a7ebf28 100644 --- a/packages/server/lib/wallet-scoring.js +++ b/packages/server/lib/wallet-scoring.js @@ -2,7 +2,7 @@ const ph = require('./plugin-helper') const argv = require('minimist')(process.argv.slice(2)) // TODO - This function should be rolled back after UI is created for this feature -function loadWalletScoring (settings) { +function loadWalletScoring(settings) { if (argv.mockScoring) { const mock = ph.load(ph.WALLET_SCORING, 'mock-scoring') return { plugin: mock, account: {} } @@ -11,7 +11,7 @@ function loadWalletScoring (settings) { const scorechainAccount = settings.accounts['scorechain'] if (scorechainAccount?.enabled) { const scorechain = ph.load(ph.WALLET_SCORING, 'scorechain') - return { plugin: scorechain, account: scorechainAccount} + return { plugin: scorechain, account: scorechainAccount } } const ellipticAccount = settings.accounts['elliptic'] @@ -20,35 +20,32 @@ function loadWalletScoring (settings) { return { plugin: elliptic, account: ellipticAccount } } -function rateTransaction (settings, cryptoCode, address) { - return Promise.resolve() - .then(() => { - const { plugin, account } = loadWalletScoring(settings) +function rateTransaction(settings, cryptoCode, address) { + return Promise.resolve().then(() => { + const { plugin, account } = loadWalletScoring(settings) - return plugin.rateAddress(account, cryptoCode, address) - }) + return plugin.rateAddress(account, cryptoCode, address) + }) } -function rateAddress (settings, cryptoCode, address) { - return Promise.resolve() - .then(() => { - const { plugin, account } = loadWalletScoring(settings) +function rateAddress(settings, cryptoCode, address) { + return Promise.resolve().then(() => { + const { plugin, account } = loadWalletScoring(settings) - return plugin.rateAddress(account, cryptoCode, address) - }) + return plugin.rateAddress(account, cryptoCode, address) + }) } -function isWalletScoringEnabled (settings, cryptoCode) { - return Promise.resolve() - .then(() => { - const { plugin, account } = loadWalletScoring(settings) +function isWalletScoringEnabled(settings, cryptoCode) { + return Promise.resolve().then(() => { + const { plugin, account } = loadWalletScoring(settings) - return plugin.isWalletScoringEnabled(account, cryptoCode) - }) + return plugin.isWalletScoringEnabled(account, cryptoCode) + }) } module.exports = { rateAddress, rateTransaction, - isWalletScoringEnabled + isWalletScoringEnabled, } diff --git a/packages/server/lib/wallet.js b/packages/server/lib/wallet.js index 0597b3ed..1f113e17 100644 --- a/packages/server/lib/wallet.js +++ b/packages/server/lib/wallet.js @@ -22,36 +22,50 @@ 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' }) +function computeSeed(masterSeed) { + return hkdf(masterSeed, 32, { + salt: 'lamassu-server-salt', + info: 'wallet-seed', + }) } -function computeOperatorId (masterSeed) { - return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex') +function computeOperatorId(masterSeed) { + return hkdf(masterSeed, 16, { + salt: 'lamassu-server-salt', + info: 'operator-id', + }).toString('hex') } -function fetchWallet (settings, cryptoCode) { - return fs.readFile(MNEMONIC_PATH, 'utf8') - .then(mnemonic => { - const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) - const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet - const wallet = ph.load(ph.WALLET, plugin) - const rawAccount = settings.accounts[plugin] - const account = _.set('seed', computeSeed(masterSeed), rawAccount) - const accountWithMnemonic = _.set('mnemonic', mnemonic, account) - if (_.isFunction(wallet.run)) wallet.run(accountWithMnemonic) - const operatorId = computeOperatorId(masterSeed) - return { wallet, account: accountWithMnemonic, operatorId } - }) +function fetchWallet(settings, cryptoCode) { + return fs.readFile(MNEMONIC_PATH, 'utf8').then(mnemonic => { + const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) + const plugin = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).wallet + const wallet = ph.load(ph.WALLET, plugin) + const rawAccount = settings.accounts[plugin] + const account = _.set('seed', computeSeed(masterSeed), rawAccount) + const accountWithMnemonic = _.set('mnemonic', mnemonic, account) + if (_.isFunction(wallet.run)) wallet.run(accountWithMnemonic) + const operatorId = computeOperatorId(masterSeed) + return { wallet, account: accountWithMnemonic, operatorId } + }) } const lastBalance = {} -function _balance (settings, cryptoCode) { +function _balance(settings, cryptoCode) { return fetchWallet(settings, cryptoCode) .then(r => r.wallet.balance(r.account, cryptoCode, settings, r.operatorId)) - .then(balance => Promise.all([balance, getOpenBatchCryptoValue(cryptoCode)])) - .then(([balance, reservedBalance]) => ({ balance: BN(balance).minus(reservedBalance), reservedBalance, timestamp: Date.now() })) + .then(balance => + Promise.all([balance, getOpenBatchCryptoValue(cryptoCode)]), + ) + .then(([balance, reservedBalance]) => ({ + balance: BN(balance).minus(reservedBalance), + reservedBalance, + timestamp: Date.now(), + })) .then(r => { lastBalance[cryptoCode] = r return r @@ -62,18 +76,24 @@ function _balance (settings, cryptoCode) { }) } -function probeLN (settings, cryptoCode, address) { +function probeLN(settings, cryptoCode, address) { return fetchWallet(settings, cryptoCode).then(r => { if (!r.wallet.probeLN) return null return r.wallet.probeLN(r.account, cryptoCode, address) }) } -function sendCoins (settings, tx) { +function sendCoins(settings, tx) { return fetchWallet(settings, tx.cryptoCode) .then(r => { - const feeMultiplier = new BN(configManager.getWalletSettings(tx.cryptoCode, settings.config).feeMultiplier) - return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier) + const feeMultiplier = new BN( + configManager.getWalletSettings( + tx.cryptoCode, + settings.config, + ).feeMultiplier, + ) + return r.wallet + .sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier) .then(res => { mem.clear(module.exports.balance) return res @@ -87,11 +107,17 @@ function sendCoins (settings, tx) { }) } -function sendCoinsBatch (settings, txs, cryptoCode) { +function sendCoinsBatch(settings, txs, cryptoCode) { return fetchWallet(settings, cryptoCode) .then(r => { - const feeMultiplier = new BN(configManager.getWalletSettings(cryptoCode, settings.config).feeMultiplier) - return r.wallet.sendCoinsBatch(r.account, txs, cryptoCode, feeMultiplier) + const feeMultiplier = new BN( + configManager.getWalletSettings( + cryptoCode, + settings.config, + ).feeMultiplier, + ) + return r.wallet + .sendCoinsBatch(r.account, txs, cryptoCode, feeMultiplier) .then(res => { mem.clear(module.exports.balance) return res @@ -106,38 +132,40 @@ function sendCoinsBatch (settings, txs, cryptoCode) { }) } -function newAddress (settings, info, tx) { - const walletAddressPromise = fetchWallet(settings, info.cryptoCode) - .then(r => r.wallet.newAddress(r.account, info, tx, settings, r.operatorId)) +function newAddress(settings, info, tx) { + const walletAddressPromise = fetchWallet(settings, info.cryptoCode).then(r => + r.wallet.newAddress(r.account, info, tx, settings, r.operatorId), + ) return Promise.all([ walletAddressPromise, - layer2.newAddress(settings, info) - ]) - .then(([toAddress, layer2Address]) => ({ - toAddress, - layer2Address - })) + layer2.newAddress(settings, info), + ]).then(([toAddress, layer2Address]) => ({ + toAddress, + layer2Address, + })) } -function newFunding (settings, cryptoCode, address) { - return fetchWallet(settings, cryptoCode) - .then(r => { - const wallet = r.wallet - const account = r.account +function newFunding(settings, cryptoCode) { + return fetchWallet(settings, cryptoCode).then(r => { + const wallet = r.wallet + const account = r.account - return wallet.newFunding(account, cryptoCode, settings, r.operatorId) - }) + return wallet.newFunding(account, cryptoCode, settings, r.operatorId) + }) } -function mergeStatus (a, b) { +function mergeStatus(a, b) { if (!a) return b if (!b) return a - return { receivedCryptoAtoms: a.receivedCryptoAtoms, status: mergeStatusMode(a.status, b.status) } + return { + receivedCryptoAtoms: a.receivedCryptoAtoms, + status: mergeStatusMode(a.status, b.status), + } } -function mergeStatusMode (a, b) { +function mergeStatusMode(a, b) { const cleared = ['authorized', 'confirmed', 'instant'] if (_.includes(a, cleared)) return a if (_.includes(b, cleared)) return b @@ -151,25 +179,30 @@ function mergeStatusMode (a, b) { return 'notSeen' } -function getWalletStatus (settings, tx) { - const fudgeFactorEnabled = configManager.getGlobalCashOut(settings.config).fudgeFactorActive +function getWalletStatus(settings, tx) { + const fudgeFactorEnabled = configManager.getGlobalCashOut( + settings.config, + ).fudgeFactorActive const fudgeFactor = fudgeFactorEnabled ? 100 : 0 const requested = tx.cryptoAtoms.minus(fudgeFactor) - const walletStatusPromise = fetchWallet(settings, tx.cryptoCode) - .then(r => r.wallet.getStatus(r.account, tx, requested, settings, r.operatorId)) + const walletStatusPromise = fetchWallet(settings, tx.cryptoCode).then(r => + r.wallet.getStatus(r.account, tx, requested, settings, r.operatorId), + ) return Promise.all([ walletStatusPromise, - layer2.getStatus(settings, tx) - ]) - .then(([walletStatus, layer2Status]) => { - return mergeStatus(walletStatus, layer2Status) - }) + layer2.getStatus(settings, tx), + ]).then(([walletStatus, layer2Status]) => { + return mergeStatus(walletStatus, layer2Status) + }) } -function authorizeZeroConf (settings, tx, machineId) { - const walletSettings = configManager.getWalletSettings(tx.cryptoCode, settings.config) +function authorizeZeroConf(settings, tx) { + const walletSettings = configManager.getWalletSettings( + tx.cryptoCode, + settings.config, + ) const isBitcoindAvailable = walletSettings.wallet === 'bitcoind' const plugin = walletSettings.zeroConf const zeroConfLimit = walletSettings.zeroConfLimit || 0 @@ -192,45 +225,60 @@ function authorizeZeroConf (settings, tx, machineId) { const zeroConf = ph.load(ph.ZERO_CONF, plugin) const account = settings.accounts[plugin] - return zeroConf.authorize(account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode, isBitcoindAvailable) + return zeroConf.authorize( + account, + tx.toAddress, + tx.cryptoAtoms, + tx.cryptoCode, + isBitcoindAvailable, + ) } -function getStatus (settings, tx, machineId) { - return getWalletStatus(settings, tx) - .then((statusRec) => { - if (statusRec.status === 'authorized') { - return authorizeZeroConf(settings, tx, machineId) - .then(isAuthorized => { - const publishAge = Date.now() - tx.publishedAt +function getStatus(settings, tx) { + return getWalletStatus(settings, tx).then(statusRec => { + if (statusRec.status === 'authorized') { + return authorizeZeroConf(settings, tx).then(isAuthorized => { + const publishAge = Date.now() - tx.publishedAt - const unauthorizedStatus = publishAge < ZERO_CONF_EXPIRATION - ? 'published' - : 'rejected' + const unauthorizedStatus = + publishAge < ZERO_CONF_EXPIRATION ? 'published' : 'rejected' - // Sanity check to confirm if there's any cryptoatoms for which to dispense bills - const authorizedStatus = isAuthorized ? 'authorized' : unauthorizedStatus - const status = BN(tx.cryptoAtoms).gt(0) ? authorizedStatus : 'rejected' + // Sanity check to confirm if there's any cryptoatoms for which to dispense bills + const authorizedStatus = isAuthorized + ? 'authorized' + : unauthorizedStatus + const status = BN(tx.cryptoAtoms).gt(0) ? authorizedStatus : 'rejected' - return { receivedCryptoAtoms: statusRec.receivedCryptoAtoms, status } - }) - } + return { receivedCryptoAtoms: statusRec.receivedCryptoAtoms, status } + }) + } - return statusRec - }) + return statusRec + }) } -function sweep (settings, txId, cryptoCode, hdIndex) { - return fetchWallet(settings, cryptoCode) - .then(r => r.wallet.sweep(r.account, txId, cryptoCode, hdIndex, settings, r.operatorId)) +function sweep(settings, txId, cryptoCode, hdIndex) { + return fetchWallet(settings, cryptoCode).then(r => + r.wallet.sweep( + r.account, + txId, + cryptoCode, + hdIndex, + settings, + r.operatorId, + ), + ) } -function isHd (settings, tx) { - return fetchWallet(settings, tx.cryptoCode) - .then(r => r.wallet.supportsHd) +function isHd(settings, tx) { + return fetchWallet(settings, tx.cryptoCode).then(r => r.wallet.supportsHd) } -function cryptoNetwork (settings, cryptoCode) { - const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet +function cryptoNetwork(settings, cryptoCode) { + const plugin = configManager.getWalletSettings( + cryptoCode, + settings.config, + ).wallet const account = settings.accounts[plugin] return fetchWallet(settings, cryptoCode).then(r => { if (!r.wallet.cryptoNetwork) return Promise.resolve(false) @@ -238,23 +286,31 @@ function cryptoNetwork (settings, cryptoCode) { }) } -function isStrictAddress (settings, cryptoCode, toAddress) { +function isStrictAddress(settings, cryptoCode, toAddress) { // Note: For now, only for wallets that specifically check for this. - return fetchWallet(settings, cryptoCode) - .then(r => { - if (!r.wallet.isStrictAddress) return true - return r.wallet.isStrictAddress(cryptoCode, toAddress, settings, r.operatorId) - }) -} - -function supportsBatching (settings, cryptoCode) { return fetchWallet(settings, cryptoCode).then(r => { - return Promise.resolve(!!r.wallet.SUPPORTS_BATCHING && !!configManager.getWalletSettings(cryptoCode, settings.config).allowTransactionBatching) + if (!r.wallet.isStrictAddress) return true + return r.wallet.isStrictAddress( + cryptoCode, + toAddress, + settings, + r.operatorId, + ) }) } -function checkBlockchainStatus (settings, cryptoCode) { +function supportsBatching(settings, cryptoCode) { + return fetchWallet(settings, cryptoCode).then(r => { + return Promise.resolve( + !!r.wallet.SUPPORTS_BATCHING && + !!configManager.getWalletSettings(cryptoCode, settings.config) + .allowTransactionBatching, + ) + }) +} + +function checkBlockchainStatus(settings, cryptoCode) { const walletDaemons = { BTC: './plugins/wallet/bitcoind/bitcoind.js', BCH: './plugins/wallet/bitcoincashd/bitcoincashd.js', @@ -262,11 +318,12 @@ function checkBlockchainStatus (settings, cryptoCode) { ETH: './plugins/wallet/geth/base.js', LTC: './plugins/wallet/litecoind/litecoind.js', XMR: './plugins/wallet/monerod/monerod.js', - ZEC: './plugins/wallet/zcashd/zcashd.js' + ZEC: './plugins/wallet/zcashd/zcashd.js', } - return Promise.resolve(require(walletDaemons[cryptoCode])) - .then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode)) + return Promise.resolve(require(walletDaemons[cryptoCode])).then( + ({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode), + ) } const balance = (settings, cryptoCode) => { @@ -286,12 +343,12 @@ const balance = (settings, cryptoCode) => { const balanceNormal = mem(_balance, { maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL * FETCH_INTERVAL, - cacheKey: (settings, cryptoCode) => cryptoCode + cacheKey: (settings, cryptoCode) => cryptoCode, }) const balanceSlow = mem(_balance, { maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW * FETCH_INTERVAL, - cacheKey: (settings, cryptoCode) => cryptoCode + cacheKey: (settings, cryptoCode) => cryptoCode, }) module.exports = { @@ -307,5 +364,5 @@ module.exports = { cryptoNetwork, supportsBatching, checkBlockchainStatus, - probeLN + probeLN, }