chore: server code formatting
This commit is contained in:
parent
aedabcbdee
commit
68517170e2
234 changed files with 9824 additions and 6195 deletions
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
316
package-lock.json
generated
316
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const version = require('../package.json').version
|
|||
logger.info('Version: %s', version)
|
||||
|
||||
function run() {
|
||||
return new Promise((resolve, reject) => {
|
||||
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)
|
||||
}
|
||||
|
|
@ -47,15 +46,15 @@ function run () {
|
|||
}
|
||||
|
||||
function loadSanctions(settings) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return Promise.resolve().then(() => {
|
||||
const triggers = configManager.getTriggers(settings.config)
|
||||
const hasSanctions = complianceTriggers.hasSanctions(triggers)
|
||||
|
||||
if (!hasSanctions) return
|
||||
|
||||
logger.info('Loading sanctions DB...')
|
||||
return ofacUpdate.update()
|
||||
return ofacUpdate
|
||||
.update()
|
||||
.then(() => logger.info('Sanctions DB updated'))
|
||||
.then(ofac.load)
|
||||
.then(() => logger.info('Sanctions DB loaded'))
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ function createAuthToken (userID, type) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
createAuthToken
|
||||
createAuthToken,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,14 +49,13 @@ const buildBillList = (units, mode) => {
|
|||
return acc
|
||||
},
|
||||
[],
|
||||
_.orderBy(['denomination'], ['desc'])(units)
|
||||
_.orderBy(['denomination'], ['desc'])(units),
|
||||
)
|
||||
case BILL_LIST_MODES.UNIT_ROUND_ROBIN:
|
||||
{
|
||||
case BILL_LIST_MODES.UNIT_ROUND_ROBIN: {
|
||||
const amountOfBills = _.reduce(
|
||||
(acc, value) => acc + value.count,
|
||||
0,
|
||||
units
|
||||
units,
|
||||
)
|
||||
|
||||
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
|
||||
|
|
@ -77,15 +75,17 @@ const buildBillList = (units, mode) => {
|
|||
|
||||
return bills
|
||||
}
|
||||
case BILL_LIST_MODES.VALUE_ROUND_ROBIN:
|
||||
{
|
||||
case BILL_LIST_MODES.VALUE_ROUND_ROBIN: {
|
||||
const amountOfBills = _.reduce(
|
||||
(acc, value) => acc + value.count,
|
||||
0,
|
||||
units
|
||||
units,
|
||||
)
|
||||
|
||||
const _units = _.flow([_.filter(it => it.count > 0), _.orderBy(['denomination'], ['asc'])])(_.cloneDeep(units))
|
||||
const _units = _.flow([
|
||||
_.filter(it => it.count > 0),
|
||||
_.orderBy(['denomination'], ['asc']),
|
||||
])(_.cloneDeep(units))
|
||||
const bills = []
|
||||
|
||||
for (let i = 0; i < amountOfBills; i++) {
|
||||
|
|
@ -116,8 +116,10 @@ const getSolution_old = (units, amount, mode) => {
|
|||
|
||||
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 }) => {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +31,7 @@ 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) {
|
||||
|
|
@ -57,5 +55,5 @@ module.exports = {
|
|||
deleteFromBlacklist,
|
||||
insertIntoBlacklist,
|
||||
getMessages,
|
||||
editBlacklistMessage
|
||||
editBlacklistMessage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const path = require('path')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
|
|
@ -13,7 +12,9 @@ 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) {
|
||||
!isDevMode() && common.firewall([coinRec.defaultPort])
|
||||
|
|
@ -27,8 +28,13 @@ 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()) {
|
||||
|
|
@ -97,13 +135,15 @@ walletrbf=1
|
|||
listenonion=0
|
||||
fallbackfee=0.00005
|
||||
rpcworkqueue=2000
|
||||
${isDevMode()
|
||||
${
|
||||
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
|
||||
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}` : ``}`
|
||||
}
|
||||
`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -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,15 +70,21 @@ 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']
|
||||
|
|
@ -114,8 +126,15 @@ function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
|
|||
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)
|
||||
|
|
@ -124,11 +143,16 @@ function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
|
|||
|
||||
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`)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@ 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..."`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ 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() {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ 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...')
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,14 +28,14 @@ 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) {
|
||||
|
|
@ -52,7 +56,10 @@ function processCryptos (codes) {
|
|||
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)
|
||||
|
||||
|
|
@ -91,20 +98,30 @@ function processCryptos (codes) {
|
|||
|
||||
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,16 +129,24 @@ 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
|
||||
|
|
@ -130,10 +155,13 @@ function isEnvironmentValid (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)
|
||||
|
|
@ -163,9 +193,14 @@ function updateCrypto (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) {
|
||||
|
|
@ -175,22 +210,25 @@ function plugin (crypto) {
|
|||
}
|
||||
|
||||
function getBlockchainSyncStatus(cryptoList) {
|
||||
return settingsLoader.loadLatest()
|
||||
.then(settings => {
|
||||
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()
|
||||
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)
|
||||
return wallet
|
||||
.checkBlockchainStatus(settings, value.cryptoCode)
|
||||
.then(res => Promise.resolve({ ...a, [value.cryptoCode]: res }))
|
||||
}
|
||||
return Promise.resolve({ ...a })
|
||||
})
|
||||
},
|
||||
Promise.resolve({}),
|
||||
cryptoList
|
||||
cryptoList,
|
||||
)
|
||||
|
||||
return blockchainStatuses
|
||||
|
|
@ -206,7 +244,11 @@ function isInstalled (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'
|
||||
}
|
||||
|
|
@ -220,33 +262,54 @@ 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))
|
||||
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 {
|
||||
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 }
|
||||
return {
|
||||
message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`,
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return { message: null, isValid: true }
|
||||
|
|
@ -256,11 +319,13 @@ function run () {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -22,15 +22,22 @@ 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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
getSatBEstimateFee,
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ 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 })
|
||||
|
|
@ -16,19 +16,19 @@ function atomic (machineTx, pi) {
|
|||
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 => {
|
||||
return t.any(sql2, [machineTx.id]).then(billRows => {
|
||||
const dbTx = cashInLow.toObj(row)
|
||||
|
||||
return preProcess(dbTx, machineTx, pi)
|
||||
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 insertNewBills(t, billRows, machineTx).then(newBills =>
|
||||
_.set('newBills', newBills, r),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -43,20 +43,30 @@ function insertNewBills (t, billRows, machineTx) {
|
|||
const dbBills = _.map(cashInLow.massage, bills)
|
||||
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, [
|
||||
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)
|
||||
_.defaultTo(0, billsByDestination.recycler6),
|
||||
])
|
||||
.then(() => {
|
||||
return t.none(sql)
|
||||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -8,22 +8,36 @@ 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 }
|
||||
|
||||
function convertBigNumFields(obj) {
|
||||
const convert = value =>
|
||||
value && BN.isBigNumber(value)
|
||||
? value.toString()
|
||||
: value
|
||||
value && BN.isBigNumber(value) ? value.toString() : value
|
||||
return _.mapValues(convert, obj)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -50,30 +72,30 @@ function toObj (row) {
|
|||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -102,11 +129,28 @@ function diff (oldTx, newTx) {
|
|||
}
|
||||
|
||||
function ensureRatchet(oldField, newField, fieldKey) {
|
||||
const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion', 'batched', 'discount']
|
||||
const free = ['sendPending', 'error', 'errorCode', 'customerId', 'discountSource']
|
||||
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,7 +158,8 @@ 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
|
||||
|
|
@ -138,9 +183,11 @@ function nilEqual (a, b) {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,17 @@ 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) {
|
||||
logger.silly('Updating cashin tx:', machineTx)
|
||||
return cashInAtomic.atomic(machineTx, pi)
|
||||
.then(r => {
|
||||
return cashInAtomic.atomic(machineTx).then(r => {
|
||||
const updatedTx = r.tx
|
||||
let addressReuse = false
|
||||
|
||||
|
|
@ -41,12 +46,17 @@ function post (machineTx, pi) {
|
|||
promises.push(
|
||||
checkForBlacklisted(updatedTx),
|
||||
doesTxReuseAddress(updatedTx),
|
||||
getWalletScore(updatedTx, pi)
|
||||
getWalletScore(updatedTx, pi),
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
||||
return Promise.all(promises).then(
|
||||
([
|
||||
config,
|
||||
blacklisted = false,
|
||||
isReusedAddress = false,
|
||||
walletScore = null,
|
||||
]) => {
|
||||
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||
const isBlacklisted = !!blacklisted
|
||||
|
||||
|
|
@ -57,16 +67,28 @@ function post (machineTx, pi) {
|
|||
addressReuse = true
|
||||
}
|
||||
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||
.then(changes =>
|
||||
_.set(
|
||||
'walletScore',
|
||||
_.isNil(walletScore) ? null : walletScore.score,
|
||||
changes,
|
||||
),
|
||||
)
|
||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||
.then(_.flow(
|
||||
.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,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -80,13 +102,12 @@ function logAction (rec, tx) {
|
|||
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) {
|
||||
|
|
@ -104,14 +125,14 @@ 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,12 +183,13 @@ 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -182,8 +205,7 @@ function doesTxReuseAddress (tx) {
|
|||
}
|
||||
|
||||
function getWalletScore(tx, pi) {
|
||||
return pi.isWalletScoringEnabled(tx)
|
||||
.then(isEnabled => {
|
||||
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
|
||||
if (!isEnabled) return null
|
||||
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
|
||||
})
|
||||
|
|
@ -203,11 +225,11 @@ 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)
|
||||
}
|
||||
|
|
@ -217,13 +239,15 @@ function cancel (txId) {
|
|||
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') +
|
||||
return (
|
||||
pgp.helpers.update(updateRec, null, 'cash_in_txs') +
|
||||
pgp.as.format(' where id=$1', [txId])
|
||||
)
|
||||
})
|
||||
.then(sql => db.result(sql, false))
|
||||
.then(res => {
|
||||
|
|
|
|||
|
|
@ -18,18 +18,27 @@ function logActionById (t, action, _rec, txId) {
|
|||
}
|
||||
|
||||
function logAction(t, action, _rec, tx) {
|
||||
const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem, device_id: tx.deviceId})
|
||||
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, {
|
||||
return logAction(
|
||||
t,
|
||||
action,
|
||||
{
|
||||
error: err.message,
|
||||
error_code: err.name
|
||||
}, tx)
|
||||
error_code: err.name,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
function mapDispense(tx) {
|
||||
|
|
@ -39,13 +48,16 @@ function mapDispense (tx) {
|
|||
|
||||
const res = {}
|
||||
|
||||
_.forEach(it => {
|
||||
_.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)))
|
||||
},
|
||||
_.times(_.identity(), _.size(bills)),
|
||||
)
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,18 +20,22 @@ function atomic (tx, pi, fromClient) {
|
|||
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)
|
||||
|
|
@ -39,51 +43,60 @@ function atomic (tx, pi, fromClient) {
|
|||
|
||||
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 => {
|
||||
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
|
||||
tx_hash: updatedTx.txHash,
|
||||
}
|
||||
|
||||
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
|
||||
}
|
||||
|
||||
const hasError = !oldTx.error && newTx.error
|
||||
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||
const hasDispenseOccurred =
|
||||
!oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||
|
||||
if (hasError || hasDispenseOccurred) {
|
||||
return cashOutActions.logDispense(t, updatedTx)
|
||||
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))
|
||||
.then(t => {
|
||||
pi.notifyOperator(updatedTx, { isRedemption: true }).catch(err =>
|
||||
logger.error('Failure sending transaction notification', err),
|
||||
)
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
|
@ -103,23 +116,29 @@ function preProcess (t, oldTx, newTx, pi) {
|
|||
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) {
|
||||
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)
|
||||
|
||||
|
|
@ -127,17 +146,29 @@ function updateCassettes (t, tx) {
|
|||
}
|
||||
|
||||
function wasJustAuthorized(oldTx, newTx, isZeroConf) {
|
||||
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published', 'rejected']) &&
|
||||
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'])
|
||||
return _.includes(status, [
|
||||
'published',
|
||||
'rejected',
|
||||
'authorized',
|
||||
'instant',
|
||||
'confirmed',
|
||||
])
|
||||
}
|
||||
|
||||
function isConfirmed(status) {
|
||||
|
|
@ -148,31 +179,38 @@ 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']
|
||||
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]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
||||
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,16 +72,18 @@ 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))
|
||||
}
|
||||
|
|
@ -82,34 +97,32 @@ function addDbBills (tx) {
|
|||
if (_.isEmpty(bills)) return tx
|
||||
|
||||
const billsObj = _.flow(
|
||||
_.reduce(
|
||||
(acc, value) => {
|
||||
_.reduce((acc, value) => {
|
||||
const suffix = _.snakeCase(value.name.replace(/cassette/gi, ''))
|
||||
return {
|
||||
...acc,
|
||||
[`provisioned_${suffix}`]: value.provisioned,
|
||||
[`denomination_${suffix}`]: value.denomination
|
||||
[`denomination_${suffix}`]: value.denomination,
|
||||
}
|
||||
},
|
||||
{}
|
||||
),
|
||||
}, {}),
|
||||
it => {
|
||||
const missingKeys = _.reduce(
|
||||
(acc, value) => {
|
||||
const missingKeys = _.reduce((acc, value) => {
|
||||
return _.assign({ [value]: 0 })(acc)
|
||||
},
|
||||
{}
|
||||
)(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it)))
|
||||
}, {})(_.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))
|
||||
const massager = _.flow(
|
||||
convertBigNumFields,
|
||||
addDbBills,
|
||||
_.omit(['direction', 'bills', 'promoCodeApplied']),
|
||||
_.mapKeys(convertField),
|
||||
)
|
||||
|
||||
return massager(tx)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,66 +7,84 @@ 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 }
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module.exports = {
|
|||
monitorLiveIncoming,
|
||||
monitorStaleIncoming,
|
||||
monitorUnnotified,
|
||||
cancel
|
||||
cancel,
|
||||
}
|
||||
|
||||
const STALE_INCOMING_TX_AGE = T.day
|
||||
|
|
@ -38,11 +38,11 @@ function selfPost (tx, pi) {
|
|||
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 => {
|
||||
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 postProcess(txVector, justAuthorized, pi).then(changes =>
|
||||
cashOutLow.update(db, newTx, changes),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -51,18 +51,20 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
|
||||
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,20 +75,31 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
.then(bills => {
|
||||
const rec = {}
|
||||
|
||||
_.forEach(it => {
|
||||
_.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)))
|
||||
},
|
||||
_.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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -103,15 +116,22 @@ 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) {
|
||||
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))
|
||||
}
|
||||
|
|
@ -124,10 +144,10 @@ function getWalletScore (tx, pi) {
|
|||
}
|
||||
|
||||
// Transaction shows up on the blockchain, we can request the sender address
|
||||
return pi.isWalletScoringEnabled(tx)
|
||||
.then(isEnabled => {
|
||||
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
|
||||
if (!isEnabled) return tx
|
||||
return pi.rateTransaction(tx)
|
||||
return pi
|
||||
.rateTransaction(tx)
|
||||
.then(res =>
|
||||
res.isValid
|
||||
? _.assign(tx, { walletScore: res.score })
|
||||
|
|
@ -135,15 +155,17 @@ function getWalletScore (tx, pi) {
|
|||
walletScore: res.score,
|
||||
error: 'Chain analysis score is above defined threshold',
|
||||
errorCode: 'scoreThresholdReached',
|
||||
dispense: true
|
||||
})
|
||||
dispense: true,
|
||||
}),
|
||||
)
|
||||
.catch(error => _.assign(tx, {
|
||||
.catch(error =>
|
||||
_.assign(tx, {
|
||||
walletScore: 10,
|
||||
error: `Failure getting address score: ${error.message}`,
|
||||
errorCode: 'walletScoringError',
|
||||
dispense: true
|
||||
}))
|
||||
dispense: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +175,20 @@ function monitorLiveIncoming (settings) {
|
|||
}
|
||||
|
||||
function monitorStaleIncoming(settings) {
|
||||
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
|
||||
return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
|
||||
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) {
|
||||
|
|
@ -179,7 +213,8 @@ 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)
|
||||
|
|
@ -189,13 +224,15 @@ 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') +
|
||||
return (
|
||||
pgp.helpers.update(updateRec, null, 'cash_out_txs') +
|
||||
pgp.as.format(' where id=$1', [txId])
|
||||
)
|
||||
})
|
||||
.then(sql => db.result(sql, false))
|
||||
.then(res => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ 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.')
|
||||
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,37 +25,71 @@ 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) {
|
||||
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,
|
||||
|
|
@ -65,14 +100,18 @@ function updateMachineWithBatch (machineContext, oldCashboxCount) {
|
|||
cashUnits.recycler4,
|
||||
cashUnits.recycler5,
|
||||
cashUnits.recycler6,
|
||||
machineContext.deviceId
|
||||
])
|
||||
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,
|
||||
|
|
@ -102,19 +141,16 @@ function editBatchById (id, performedBy) {
|
|||
}
|
||||
|
||||
function logFormatter(data) {
|
||||
return _.map(
|
||||
it => {
|
||||
return _.map(it => {
|
||||
return {
|
||||
id: it.id,
|
||||
deviceId: it.deviceId,
|
||||
created: it.created,
|
||||
operationType: it.operationType,
|
||||
billCount: it.billCount,
|
||||
fiatTotal: it.fiatTotal
|
||||
fiatTotal: it.fiatTotal,
|
||||
}
|
||||
},
|
||||
data
|
||||
)
|
||||
}, data)
|
||||
}
|
||||
|
||||
function getMachineUnbatchedBills(deviceId) {
|
||||
|
|
@ -125,7 +161,8 @@ 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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
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,15 +70,18 @@ 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--) {
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ module.exports = { update }
|
|||
|
||||
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,7 +46,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
|||
cashInFixedFee,
|
||||
cashOutFixedFee,
|
||||
cashInRate,
|
||||
cashOutRate
|
||||
cashOutRate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +58,7 @@ function mapIdentification (config) {
|
|||
isPalmVein: false,
|
||||
isPhoto: complianceTriggers.hasFacephoto(triggers),
|
||||
isIdDocScan: complianceTriggers.hasIdScan(triggers),
|
||||
isFingerprint: false
|
||||
isFingerprint: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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,7 +109,7 @@ function mapMachine (rates, settings, machineRow) {
|
|||
cashOutDailyLimit: cashLimit,
|
||||
fiatCurrency: locale.fiatCurrency,
|
||||
identification,
|
||||
coins
|
||||
coins,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +118,8 @@ function getMachines (rates, settings) {
|
|||
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])))
|
||||
}
|
||||
|
||||
|
|
@ -123,26 +135,27 @@ 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) {
|
||||
const timestamp = new Date().toISOString()
|
||||
return Promise.all([getMachines(rates, settings), getOperatorId('coinatmradar')])
|
||||
.then(([machines, operatorId]) => {
|
||||
return Promise.all([
|
||||
getMachines(rates, settings),
|
||||
getOperatorId('coinatmradar'),
|
||||
]).then(([machines, operatorId]) => {
|
||||
return {
|
||||
operatorId: operatorId,
|
||||
operator: {
|
||||
name: null,
|
||||
phone: null,
|
||||
email: null
|
||||
email: null,
|
||||
},
|
||||
timestamp,
|
||||
machines
|
||||
machines,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 +252,12 @@ function validateData(data) {
|
|||
test('Verify axios request schema', async () => {
|
||||
const axios = require('axios')
|
||||
|
||||
jest.spyOn(axios, 'default').mockImplementation(
|
||||
jest
|
||||
.spyOn(axios, 'default')
|
||||
.mockImplementation(
|
||||
jest.fn(req =>
|
||||
validateData(req.data)
|
||||
.then(() => ({ status: 'mock status 200' }))
|
||||
.catch(e => fail(e))
|
||||
)
|
||||
validateData(req.data).then(() => ({ status: 'mock status 200' })),
|
||||
),
|
||||
)
|
||||
|
||||
db.any.mockResolvedValue(dbResponse)
|
||||
|
|
|
|||
|
|
@ -9,21 +9,31 @@ function truncateCrypto (cryptoAtoms, cryptoCode) {
|
|||
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) {
|
||||
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) {
|
||||
|
|
@ -36,5 +46,5 @@ function getDiscountRate (discount, commission) {
|
|||
|
||||
module.exports = {
|
||||
fiatToCrypto,
|
||||
getDiscountRate
|
||||
getDiscountRate,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,23 +39,17 @@ 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 Promise.all(applicantPromises).then(applicantResults => {
|
||||
return _.reduce((map, result) => {
|
||||
if (result.status) map[result.service] = result.status
|
||||
return map
|
||||
|
|
@ -76,5 +75,5 @@ module.exports = {
|
|||
getStatusMap,
|
||||
getStatus,
|
||||
createApplicant,
|
||||
createLink
|
||||
createLink,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
function getBackwardsCompatibleTriggers(triggers) {
|
||||
const filtered = _.filter(_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }))(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) {
|
||||
|
|
@ -15,12 +19,17 @@ function maxDaysThreshold (triggers) {
|
|||
}
|
||||
|
||||
function getCashLimit(triggers) {
|
||||
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
|
||||
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
|
||||
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 }
|
||||
module.exports = {
|
||||
getBackwardsCompatibleTriggers,
|
||||
hasSanctions,
|
||||
maxDaysThreshold,
|
||||
getCashLimit,
|
||||
hasPhone,
|
||||
hasFacephoto,
|
||||
hasIdScan,
|
||||
AUTH_METHODS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,26 @@ function logSanctionsMatch (deviceId, customer, sanctionsId, alias) {
|
|||
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)
|
||||
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(() => {
|
||||
return Promise.resolve().then(() => {
|
||||
// Probably because we haven't asked for ID yet
|
||||
if (!_.isPlainObject(customer.idCardData)) {
|
||||
return true
|
||||
|
|
@ -31,31 +38,38 @@ function matchOfac (deviceId, customer) {
|
|||
|
||||
const nameParts = {
|
||||
firstName: customer.idCardData.firstName,
|
||||
lastName: customer.idCardData.lastName
|
||||
lastName: customer.idCardData.lastName,
|
||||
}
|
||||
|
||||
if (_.some(_.isNil, _.values(nameParts))) {
|
||||
logger.error(new Error(`Insufficient idCardData while matching OFAC for: ${customer.id}`))
|
||||
logger.error(
|
||||
new Error(
|
||||
`Insufficient idCardData while matching OFAC for: ${customer.id}`,
|
||||
),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
const birthDate = customer.idCardData.dateOfBirth
|
||||
|
||||
if (_.isNil(birthDate)) {
|
||||
logger.error(new Error(`No birth date while matching OFAC for: ${customer.id}`))
|
||||
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
|
||||
debug: false,
|
||||
}
|
||||
|
||||
const results = ofac.match(nameParts, birthDate, options)
|
||||
|
||||
return logSanctionsMatches(deviceId, customer, results)
|
||||
.then(() => !_.isEmpty(results))
|
||||
return logSanctionsMatches(deviceId, customer, results).then(
|
||||
() => !_.isEmpty(results),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -63,16 +77,14 @@ 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 } :
|
||||
{}
|
||||
return validateOfac(deviceId, customer).then(sanctions =>
|
||||
_.isNil(customer.sanctions) || customer.sanctions !== sanctions
|
||||
? { sanctions }
|
||||
: {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function add (complianceOverride) {
|
|||
complianceOverride.customerId,
|
||||
complianceOverride.complianceType,
|
||||
complianceOverride.overrideBy,
|
||||
complianceOverride.verification
|
||||
complianceOverride.verification,
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -38,15 +42,15 @@ 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)
|
||||
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)
|
||||
const sql =
|
||||
'insert into customers (id, email, email_at) values ($1, $2, now()) returning *'
|
||||
return db.one(sql, [uuid.v4(), customer.email]).then(camelize)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,14 +66,12 @@ function addWithEmail (customer) {
|
|||
*/
|
||||
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) {
|
||||
const sql = 'select * from customers where email=$1'
|
||||
return db.oneOrNone(sql, [email])
|
||||
.then(camelize)
|
||||
return db.oneOrNone(sql, [email]).then(camelize)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,13 +89,17 @@ function getWithEmail (email) {
|
|||
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)
|
||||
|
|
@ -123,15 +129,19 @@ 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') +
|
||||
const sql =
|
||||
Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||
' where id=$1'
|
||||
|
||||
await db.none(sql, [id])
|
||||
|
|
@ -160,25 +170,31 @@ function edit (id, data, userToken) {
|
|||
'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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -195,7 +211,7 @@ function edit (id, data, userToken) {
|
|||
|
||||
function enhanceEditedFields(fields, userToken) {
|
||||
if (!userToken) return fields
|
||||
_.mapKeys((field) => {
|
||||
_.mapKeys(field => {
|
||||
fields[field + '_by'] = userToken
|
||||
fields[field + '_at'] = 'now()^'
|
||||
}, fields)
|
||||
|
|
@ -213,7 +229,7 @@ function enhanceEditedFields (fields, userToken) {
|
|||
*/
|
||||
|
||||
function enhanceEditedPhotos(fields) {
|
||||
return _.mapKeys((field) => {
|
||||
return _.mapKeys(field => {
|
||||
if (_.includes(field, ['front_camera', 'id_card_photo'])) {
|
||||
return field + '_path'
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -269,7 +286,8 @@ function deleteEditedData (id, data) {
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
@ -317,7 +330,8 @@ const updateSubscriberData = (customerId, data, userToken) => {
|
|||
*/
|
||||
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)
|
||||
|
|
@ -339,14 +353,11 @@ function camelize (customer) {
|
|||
}
|
||||
|
||||
function camelizeDeep(customer) {
|
||||
return _.flow(
|
||||
camelize,
|
||||
it => ({
|
||||
return _.flow(camelize, it => ({
|
||||
...it,
|
||||
notes: (it.notes ?? []).map(camelize),
|
||||
externalCompliance: (it.externalCompliance ?? []).map(camelize)
|
||||
})
|
||||
)(customer)
|
||||
externalCompliance: (it.externalCompliance ?? []).map(camelize),
|
||||
}))(customer)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -367,7 +378,8 @@ function getComplianceTypes () {
|
|||
'front_camera',
|
||||
'sanctions',
|
||||
'authorized',
|
||||
'us_ssn' ]
|
||||
'us_ssn',
|
||||
]
|
||||
}
|
||||
|
||||
function updateOverride(fields) {
|
||||
|
|
@ -376,14 +388,18 @@ function updateOverride (fields) {
|
|||
'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)
|
||||
}
|
||||
|
|
@ -395,7 +411,7 @@ function enhanceAtFields (fields) {
|
|||
'front_camera',
|
||||
'sanctions',
|
||||
'authorized',
|
||||
'us_ssn'
|
||||
'us_ssn',
|
||||
]
|
||||
|
||||
const updatedFields = _.intersection(updateableFields, _.keys(fields))
|
||||
|
|
@ -418,14 +434,18 @@ function enhanceAtFields (fields) {
|
|||
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'])
|
||||
return _.reduce(
|
||||
_.assign,
|
||||
fields,
|
||||
_.map(type => {
|
||||
return fields[type + '_override']
|
||||
? {
|
||||
[type + '_override_by']: userToken,
|
||||
[type + '_override_at']: 'now()^'
|
||||
[type + '_override_at']: 'now()^',
|
||||
}
|
||||
: {}
|
||||
}, getComplianceTypes()))
|
||||
}, getComplianceTypes()),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -447,20 +467,22 @@ function addComplianceOverrides (id, customer, userToken) {
|
|||
// Prepare compliance overrides to save
|
||||
const overrides = _.map(field => {
|
||||
const complianceName = field + '_override'
|
||||
return (customer[complianceName]) ? {
|
||||
return customer[complianceName]
|
||||
? {
|
||||
customerId: id,
|
||||
complianceType: field,
|
||||
overrideBy: userToken,
|
||||
verification: customer[complianceName]
|
||||
} : null
|
||||
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
|
||||
*
|
||||
|
|
@ -474,19 +496,20 @@ 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) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -561,7 +606,10 @@ 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(',')
|
||||
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)
|
||||
|
|
@ -605,8 +654,9 @@ function getCustomerById (id) {
|
|||
}
|
||||
|
||||
function assignCustomerData(customer) {
|
||||
return getEditedData(customer.id)
|
||||
.then(customerEditedData => selectLatestData(customer, customerEditedData))
|
||||
return getEditedData(customer.id).then(customerEditedData =>
|
||||
selectLatestData(customer, customerEditedData),
|
||||
)
|
||||
}
|
||||
|
||||
function formatSubscriberInfo(customer) {
|
||||
|
|
@ -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
|
||||
|
|
@ -638,8 +688,7 @@ function formatSubscriberInfo(customer) {
|
|||
*/
|
||||
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) {
|
||||
|
|
@ -649,20 +698,20 @@ function selectLatestData (customerData, customerEditedData) {
|
|||
'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
|
||||
}
|
||||
|
||||
|
|
@ -672,8 +721,7 @@ function selectLatestData (customerData, customerEditedData) {
|
|||
* @returns {Promise<Object>} new patch to be applied
|
||||
*/
|
||||
function updatePhotoCard(id, patch) {
|
||||
return Promise.resolve(patch)
|
||||
.then(patch => {
|
||||
return Promise.resolve(patch).then(patch => {
|
||||
// Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ..
|
||||
const imageData = _.get('idCardPhotoData', patch)
|
||||
|
||||
|
|
@ -689,14 +737,14 @@ function updatePhotoCard (id, patch) {
|
|||
|
||||
// workout the image hash
|
||||
// i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(imageData)
|
||||
.digest('hex')
|
||||
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)))))
|
||||
const rpath = _.join(
|
||||
path.sep,
|
||||
_.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))),
|
||||
)
|
||||
|
||||
// i.e. ../<lamassu-server-home>/idphotocard/24/0e/85
|
||||
const dirname = path.join(ID_PHOTO_CARD_DIR, rpath)
|
||||
|
|
@ -716,8 +764,7 @@ function updatePhotoCard (id, patch) {
|
|||
newPatch.idCardPhotoAt = 'now()'
|
||||
|
||||
// write image file
|
||||
return writeFile(filename, decodedImageData)
|
||||
.then(() => newPatch)
|
||||
return writeFile(filename, decodedImageData).then(() => newPatch)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -727,8 +774,7 @@ function updatePhotoCard (id, patch) {
|
|||
*/
|
||||
|
||||
function updatePhotos(imagesData, id, dir) {
|
||||
return Promise.resolve(imagesData)
|
||||
.then(imagesData => {
|
||||
return Promise.resolve(imagesData).then(imagesData => {
|
||||
const newPatch = {}
|
||||
if (_.isEmpty(imagesData)) {
|
||||
return newPatch
|
||||
|
|
@ -745,8 +791,7 @@ function updatePhotos (imagesData, id, dir) {
|
|||
return writeFile(filename, decodedImageData)
|
||||
})
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(arr => {
|
||||
return Promise.all(promises).then(() => {
|
||||
newPatch.idCardData = path.join(dirname)
|
||||
newPatch.idCardDataAt = 'now()'
|
||||
return newPatch
|
||||
|
|
@ -764,11 +809,11 @@ function updateIdCardData (patch, id) {
|
|||
const operatorId = 'id-operator'
|
||||
const directory = `${OPERATOR_DATA_DIR}/${operatorId}/${id}/`
|
||||
|
||||
return Promise.resolve(patch)
|
||||
.then(patch => {
|
||||
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 updatePhotos(imagesData, id, directory).catch(err =>
|
||||
logger.error('while saving the image: ', err),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -777,8 +822,7 @@ function updateIdCardData (patch, id) {
|
|||
* @returns {Promise<Object>} new patch to be applied
|
||||
*/
|
||||
function updateTxCustomerPhoto(imageData) {
|
||||
return Promise.resolve(imageData)
|
||||
.then(imageData => {
|
||||
return Promise.resolve(imageData).then(imageData => {
|
||||
const newPatch = {}
|
||||
const directory = `${OPERATOR_DATA_DIR}/customersphotos`
|
||||
|
||||
|
|
@ -791,14 +835,14 @@ function updateTxCustomerPhoto (imageData) {
|
|||
|
||||
// workout the image hash
|
||||
// i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(imageData)
|
||||
.digest('hex')
|
||||
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)))))
|
||||
const rpath = _.join(
|
||||
path.sep,
|
||||
_.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))),
|
||||
)
|
||||
|
||||
// i.e. ../<lamassu-server-home>/<operator-dir>/customersphotos/24/0e/85
|
||||
const dirname = path.join(directory, rpath)
|
||||
|
|
@ -818,14 +862,12 @@ function updateTxCustomerPhoto (imageData) {
|
|||
newPatch.txCustomerPhotoAt = 'now()'
|
||||
|
||||
// write image file
|
||||
return writeFile(filename, decodedImageData)
|
||||
.then(() => newPatch)
|
||||
return writeFile(filename, decodedImageData).then(() => newPatch)
|
||||
})
|
||||
}
|
||||
|
||||
function updateFrontCamera(id, patch) {
|
||||
return Promise.resolve(patch)
|
||||
.then(patch => {
|
||||
return Promise.resolve(patch).then(patch => {
|
||||
// Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ..
|
||||
const imageData = _.get('frontCameraData', patch)
|
||||
|
||||
|
|
@ -841,14 +883,14 @@ function updateFrontCamera (id, patch) {
|
|||
|
||||
// workout the image hash
|
||||
// i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(imageData)
|
||||
.digest('hex')
|
||||
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)))))
|
||||
const rpath = _.join(
|
||||
path.sep,
|
||||
_.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))),
|
||||
)
|
||||
|
||||
// i.e. ../<lamassu-server-home>/idphotocard/24/0e/85
|
||||
const dirname = path.join(FRONT_CAMERA_DIR, rpath)
|
||||
|
|
@ -868,31 +910,47 @@ function updateFrontCamera (id, patch) {
|
|||
newPatch.frontCameraAt = 'now()'
|
||||
|
||||
// write image file
|
||||
return writeFile(filename, decodedImageData)
|
||||
.then(() => newPatch)
|
||||
return writeFile(filename, decodedImageData).then(() => newPatch)
|
||||
})
|
||||
}
|
||||
|
||||
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 => {
|
||||
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])
|
||||
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])
|
||||
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])
|
||||
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))
|
||||
}
|
||||
|
|
@ -904,24 +962,36 @@ function saveCustomField (customerId, fieldId, newValue) {
|
|||
|
||||
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) {
|
||||
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) {
|
||||
|
|
@ -940,11 +1010,12 @@ function updateLastAuthAttempt (customerId, deviceId) {
|
|||
}
|
||||
|
||||
function getExternalComplianceMachine(customer) {
|
||||
return settingsLoader.loadLatest()
|
||||
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,10 +1058,12 @@ 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]) => {
|
||||
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 })
|
||||
|
|
@ -996,8 +1072,7 @@ function notifyRetryExternalCompliance(settings, customerId, service) {
|
|||
|
||||
function notifyApprovedExternalCompliance(settings, customerId) {
|
||||
const sql = 'SELECT phone FROM customers WHERE id=$1'
|
||||
return db.one(sql, [customerId])
|
||||
.then((toNumber) => {
|
||||
return db.one(sql, [customerId]).then(toNumber => {
|
||||
const body = 'Your external compliance verification has been approved.'
|
||||
|
||||
return sms.sendMessage(settings, { toNumber, body })
|
||||
|
|
@ -1005,17 +1080,27 @@ function notifyApprovedExternalCompliance(settings, customerId) {
|
|||
}
|
||||
|
||||
function checkExternalCompliance(settings) {
|
||||
return getOpenExternalCompliance()
|
||||
.then(externals => {
|
||||
return getOpenExternalCompliance().then(externals => {
|
||||
console.log(externals)
|
||||
const promises = _.map(external => {
|
||||
return externalCompliance.getStatus(settings, external.service, external.customer_id)
|
||||
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)
|
||||
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)
|
||||
return updateExternalCompliance(
|
||||
external.customer_id,
|
||||
external.service,
|
||||
status.status.answer,
|
||||
)
|
||||
})
|
||||
}, externals)
|
||||
return Promise.all(promises)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
const ph = require('./plugin-helper')
|
||||
|
||||
function sendMessage(settings, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
return Promise.resolve().then(() => {
|
||||
const pluginCode =
|
||||
settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
|
|
@ -12,9 +12,9 @@ function sendMessage (settings, rec) {
|
|||
}
|
||||
|
||||
function sendCustomerMessage(settings, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
return Promise.resolve().then(() => {
|
||||
const pluginCode =
|
||||
settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ module.exports = {
|
|||
isDevMode,
|
||||
isProdMode,
|
||||
isRemoteNode,
|
||||
isRemoteWallet
|
||||
isRemoteWallet,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -7,16 +7,19 @@ 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
|
||||
const exchange = configManager.getWalletSettings(
|
||||
cryptoCode,
|
||||
settings.config,
|
||||
).exchange
|
||||
if (exchange === 'no-exchange') return null
|
||||
return exchange
|
||||
}
|
||||
|
||||
function fetchExchange(settings, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return Promise.resolve().then(() => {
|
||||
const exchangeName = lookupExchange(settings, cryptoCode)
|
||||
if (exchangeName === 'mock-exchange') return { exchangeName, account: { currencyMarket: 'EUR' } }
|
||||
if (exchangeName === 'mock-exchange')
|
||||
return { exchangeName, account: { currencyMarket: 'EUR' } }
|
||||
if (!exchangeName) throw new Error('No exchange set')
|
||||
const account = settings.accounts[exchangeName]
|
||||
|
||||
|
|
@ -26,8 +29,7 @@ function fetchExchange (settings, cryptoCode) {
|
|||
|
||||
function buy(settings, tradeEntry) {
|
||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||
return fetchExchange(settings, cryptoCode)
|
||||
.then(r => {
|
||||
return fetchExchange(settings, cryptoCode).then(r => {
|
||||
if (r.exchangeName === 'mock-exchange') {
|
||||
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
|
||||
}
|
||||
|
|
@ -37,8 +39,7 @@ function buy (settings, tradeEntry) {
|
|||
|
||||
function sell(settings, tradeEntry) {
|
||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||
return fetchExchange(settings, cryptoCode)
|
||||
.then(r => {
|
||||
return fetchExchange(settings, cryptoCode).then(r => {
|
||||
if (r.exchangeName === 'mock-exchange') {
|
||||
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
|
||||
}
|
||||
|
|
@ -51,26 +52,34 @@ function active (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))
|
||||
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 }) => ({
|
||||
const transformToObject = _.reduce(
|
||||
(acc, { exchange, markets }) => ({
|
||||
...acc,
|
||||
[exchange]: markets
|
||||
}), {})
|
||||
[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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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
|
||||
fxRate,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,12 +64,17 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => {
|
|||
|
||||
if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`)
|
||||
|
||||
return activeAPI(fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty)
|
||||
.catch(() => {
|
||||
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}`)
|
||||
if (retries >= MAX_ROTATIONS)
|
||||
throw new Error(`FOREX API error from ${erroredService.name}`)
|
||||
|
||||
return getRate(++retries, fiatCode)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,48 +62,51 @@ 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(
|
||||
const addMachineScreenOpts = smth =>
|
||||
_.update(
|
||||
'screenOptions',
|
||||
_.flow(
|
||||
addSmthInfo(
|
||||
'rates',
|
||||
[
|
||||
'active'
|
||||
]
|
||||
)(smth.rates)
|
||||
)
|
||||
_.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 => {
|
||||
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 }
|
||||
customTriggers[index] = {
|
||||
...customTriggers[index],
|
||||
customInfoRequest: details,
|
||||
}
|
||||
})
|
||||
return [...normalTriggers, ...customTriggers]
|
||||
})
|
||||
}
|
||||
|
||||
const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => {
|
||||
const massageCoins = _.map(_.pick([
|
||||
const staticConfig = ({
|
||||
currentConfigVersion,
|
||||
deviceId,
|
||||
deviceName,
|
||||
pq,
|
||||
settings,
|
||||
}) => {
|
||||
const massageCoins = _.map(
|
||||
_.pick([
|
||||
'batchable',
|
||||
'cashInCommission',
|
||||
'cashInFee',
|
||||
|
|
@ -109,34 +118,34 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
'cryptoUnits',
|
||||
'display',
|
||||
'minimumTx',
|
||||
'isCashInOnly'
|
||||
]))
|
||||
'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,9 +158,9 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
{ numberOfCassettes, numberOfRecyclers },
|
||||
customerAuthentication,
|
||||
]) =>
|
||||
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
||||
null :
|
||||
_.flow(
|
||||
currentConfigVersion && currentConfigVersion >= staticConf.configVersion
|
||||
? null
|
||||
: _.flow(
|
||||
_.assign({
|
||||
enablePaperWalletOnly,
|
||||
triggersAutomation,
|
||||
|
|
@ -160,9 +169,14 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
localeInfo: {
|
||||
country: localeInfo.country,
|
||||
languages: localeInfo.languages,
|
||||
fiatCode: localeInfo.fiatCurrency
|
||||
fiatCode: localeInfo.fiatCurrency,
|
||||
},
|
||||
machineInfo: {
|
||||
deviceId,
|
||||
deviceName,
|
||||
numberOfCassettes,
|
||||
numberOfRecyclers,
|
||||
},
|
||||
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers },
|
||||
twoWayMode,
|
||||
customerAuthentication,
|
||||
speedtestFiles,
|
||||
|
|
@ -170,67 +184,95 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
}),
|
||||
addOperatorInfo(operatorInfo),
|
||||
addReceiptInfo(receiptInfo),
|
||||
addMachineScreenOpts(machineScreenOpts)
|
||||
)(staticConf))
|
||||
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),
|
||||
cassettes
|
||||
? _.flow(
|
||||
cassettes =>
|
||||
_.set('physical', _.get('cassettes', cassettes), cassettes),
|
||||
cassettes =>
|
||||
_.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
|
||||
_.unset('cassettes'),
|
||||
_.unset('virtualCassettes')
|
||||
)(cassettes) :
|
||||
null
|
||||
_.unset('virtualCassettes'),
|
||||
)(cassettes)
|
||||
: null
|
||||
|
||||
const massageRecyclers = recyclers =>
|
||||
recyclers ?
|
||||
_.flow(
|
||||
recyclers => _.set('physical', _.get('recyclers', recyclers), recyclers),
|
||||
recyclers => _.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
|
||||
recyclers
|
||||
? _.flow(
|
||||
recyclers =>
|
||||
_.set('physical', _.get('recyclers', recyclers), recyclers),
|
||||
recyclers =>
|
||||
_.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
|
||||
_.unset('recyclers'),
|
||||
_.unset('virtualRecyclers')
|
||||
)(recyclers) :
|
||||
null
|
||||
_.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(
|
||||
_.update(
|
||||
'balances',
|
||||
_.flow(
|
||||
_.toPairs,
|
||||
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }])
|
||||
)),
|
||||
_.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) ? ({
|
||||
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
|
||||
}
|
||||
: 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,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -32,5 +32,5 @@ module.exports = {
|
|||
getHardwareCredentials,
|
||||
getHardwareCredentialsByUserId,
|
||||
getUserByUserHandle,
|
||||
updateHardwareCredential
|
||||
updateHardwareCredential,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ const ph = require('./plugin-helper')
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
function fetch(settings, cryptoCode) {
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
|
||||
const plugin = configManager.getWalletSettings(
|
||||
cryptoCode,
|
||||
settings.config,
|
||||
).layer2
|
||||
|
||||
if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve()
|
||||
|
||||
|
|
@ -14,8 +17,7 @@ function fetch (settings, cryptoCode) {
|
|||
}
|
||||
|
||||
function newAddress(settings, info) {
|
||||
return fetch(settings, info.cryptoCode)
|
||||
.then(r => {
|
||||
return fetch(settings, info.cryptoCode).then(r => {
|
||||
if (!r) return
|
||||
return r.layer2.newAddress(r.account, info)
|
||||
})
|
||||
|
|
@ -25,15 +27,22 @@ function getStatus (settings, tx) {
|
|||
const toAddress = tx.layer2Address
|
||||
if (!toAddress) return Promise.resolve({ status: 'notSeen' })
|
||||
|
||||
return fetch(settings, tx.cryptoCode)
|
||||
.then(r => {
|
||||
return fetch(settings, tx.cryptoCode).then(r => {
|
||||
if (!r) return { status: 'notSeen' }
|
||||
return r.layer2.getStatus(r.account, toAddress, tx.cryptoAtoms, tx.cryptoCode)
|
||||
return r.layer2.getStatus(
|
||||
r.account,
|
||||
toAddress,
|
||||
tx.cryptoAtoms,
|
||||
tx.cryptoCode,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function cryptoNetwork(settings, cryptoCode) {
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
|
||||
const plugin = configManager.getWalletSettings(
|
||||
cryptoCode,
|
||||
settings.config,
|
||||
).layer2
|
||||
const layer2 = ph.load(ph.LAYER2, plugin)
|
||||
const account = settings.accounts[plugin]
|
||||
|
||||
|
|
@ -49,5 +58,5 @@ module.exports = {
|
|||
isLayer2Address,
|
||||
newAddress,
|
||||
getStatus,
|
||||
cryptoNetwork
|
||||
cryptoNetwork,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,18 @@ 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]) => {
|
||||
|
|
@ -28,19 +28,21 @@ const logger = new winston.Logger({
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -24,8 +23,11 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -41,9 +43,10 @@ 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'})
|
||||
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)
|
||||
|
|
@ -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]) => ({
|
||||
return Promise.all([
|
||||
db.any(sql, [deviceId, until]),
|
||||
getMachineName(deviceId),
|
||||
]).then(([logs, machineName]) => ({
|
||||
logs: _.map(_.mapKeys(_.camelCase), logs),
|
||||
currentMachine: {deviceId, name: machineName}
|
||||
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]) => ({
|
||||
return Promise.all([
|
||||
db.any(sql, [deviceId, until, limit, offset]),
|
||||
getMachineName(deviceId),
|
||||
]).then(([logs, machineName]) => ({
|
||||
logs: _.map(_.mapKeys(_.camelCase), logs),
|
||||
currentMachine: {deviceId, name: machineName}
|
||||
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) {
|
||||
return _.map(log => {
|
||||
const values = _.map(
|
||||
field =>
|
||||
{
|
||||
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...`)
|
||||
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
|
||||
)
|
||||
}, 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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
const db = require('./db')
|
||||
const uuid = require('uuid')
|
||||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
function getAvailablePromoCodes() {
|
||||
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
|
||||
|
|
@ -30,11 +29,16 @@ function getNumberOfAvailablePromoCodes () {
|
|||
|
||||
function getAvailableIndividualDiscounts() {
|
||||
const sql = `SELECT * FROM individual_discounts WHERE soft_deleted=false`
|
||||
return db.any(sql).then(res => _.map(it => ({
|
||||
return db.any(sql).then(res =>
|
||||
_.map(
|
||||
it => ({
|
||||
id: it.id,
|
||||
customerId: it.customer_id,
|
||||
discount: it.discount
|
||||
}), res))
|
||||
discount: it.discount,
|
||||
}),
|
||||
res,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function getCustomerActiveIndividualDiscount(customerId) {
|
||||
|
|
@ -44,7 +48,7 @@ function getCustomerActiveIndividualDiscount (customerId) {
|
|||
return {
|
||||
id: res.id,
|
||||
customerId: res.customer_id,
|
||||
discount: res.discount
|
||||
discount: res.discount,
|
||||
}
|
||||
}
|
||||
return res
|
||||
|
|
@ -70,5 +74,5 @@ module.exports = {
|
|||
getAvailableIndividualDiscounts,
|
||||
getCustomerActiveIndividualDiscount,
|
||||
createIndividualDiscount,
|
||||
deleteIndividualDiscount
|
||||
deleteIndividualDiscount,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
@ -51,21 +51,27 @@ 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,
|
||||
}
|
||||
|
|
@ -78,18 +84,20 @@ function getMachineIds () {
|
|||
|
||||
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 =>
|
||||
return db
|
||||
.any('SELECT * FROM unpaired_devices')
|
||||
.then(
|
||||
_.map(r =>
|
||||
_.flow(
|
||||
_.set('deviceId', _.get('device_id', r)),
|
||||
_.unset('device_id')
|
||||
)(r)
|
||||
))
|
||||
_.unset('device_id'),
|
||||
)(r),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function getConfig(defaultConfig) {
|
||||
|
|
@ -113,8 +121,8 @@ 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 })
|
||||
|
|
@ -122,13 +130,29 @@ function addName (pings, events, config) {
|
|||
}
|
||||
|
||||
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]
|
||||
))
|
||||
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))
|
||||
})
|
||||
|
|
@ -146,20 +170,27 @@ function getMachineNames (config) {
|
|||
*/
|
||||
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) {
|
||||
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]) => {
|
||||
return Promise.all([
|
||||
queryMachine,
|
||||
dbm.machineEvents(),
|
||||
config,
|
||||
getNetworkHeartbeatByDevice(machineId),
|
||||
getNetworkPerformanceByDevice(machineId),
|
||||
]).then(([machine, events, config, heartbeat, performance]) => {
|
||||
const pings = checkPings([machine])
|
||||
const mergedMachine = {
|
||||
...machine,
|
||||
|
|
@ -179,32 +210,91 @@ function renameMachine (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 {
|
||||
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()) {
|
||||
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 }) {
|
||||
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(() => ({
|
||||
return _.concat(
|
||||
acc,
|
||||
_.times(
|
||||
() => ({
|
||||
id: uuid.v4(),
|
||||
fiat: unit.denomination,
|
||||
fiat_code: fiatCode,
|
||||
device_id: deviceId
|
||||
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)))
|
||||
}),
|
||||
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,10 +347,16 @@ 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`, [
|
||||
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),
|
||||
|
|
@ -263,8 +367,9 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
|||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||
deviceId
|
||||
])
|
||||
deviceId,
|
||||
],
|
||||
)
|
||||
|
||||
return t.batch([q1, q2, q3])
|
||||
})
|
||||
|
|
@ -272,25 +377,31 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
|||
}
|
||||
|
||||
function refillMachineUnits({ deviceId, newUnits }) {
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
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]
|
||||
}
|
||||
delta: newUnits[value] - machine.cashUnits[value],
|
||||
},
|
||||
}),
|
||||
{},
|
||||
_.keys(newUnits)
|
||||
_.keys(newUnits),
|
||||
)
|
||||
|
||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills)))
|
||||
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
|
||||
operation_type: it,
|
||||
}))(operationNames)
|
||||
|
||||
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
|
||||
|
|
@ -300,8 +411,12 @@ function refillMachineUnits ({ deviceId, newUnits }) {
|
|||
|
||||
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 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`,
|
||||
[
|
||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||
|
|
@ -312,8 +427,9 @@ function refillMachineUnits ({ deviceId, newUnits }) {
|
|||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||
deviceId
|
||||
])
|
||||
deviceId,
|
||||
],
|
||||
)
|
||||
|
||||
return t.batch([q1, q2])
|
||||
})
|
||||
|
|
@ -325,48 +441,53 @@ function unpair (rec) {
|
|||
}
|
||||
|
||||
function reboot(rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
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(
|
||||
{
|
||||
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(
|
||||
{
|
||||
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(
|
||||
{
|
||||
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(
|
||||
{
|
||||
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) {
|
||||
|
|
@ -391,28 +512,42 @@ function diagnostics (rec) {
|
|||
|
||||
return Promise.all(removeFiles)
|
||||
.then(() => db.none(sql, [rec.deviceId]))
|
||||
.then(() => db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
.then(() =>
|
||||
db.none('NOTIFY $1:name, $2', [
|
||||
'machineAction',
|
||||
JSON.stringify({
|
||||
action: 'diagnostics',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)]))
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,11 +557,14 @@ function updateNetworkPerformance (deviceId, 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)
|
||||
|
|
@ -440,7 +578,7 @@ 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)
|
||||
|
|
@ -448,30 +586,36 @@ function updateNetworkHeartbeat (deviceId, data) {
|
|||
|
||||
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() {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
|
|
@ -484,15 +628,21 @@ 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))
|
||||
}
|
||||
|
||||
|
|
@ -510,9 +660,9 @@ function createPhoto (name, data, dir) {
|
|||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ const logger = require('../logger')
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,17 @@ function filterOldRequests (req, res, next) {
|
|||
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' })
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,25 @@ const state = require('./state')
|
|||
const newSettingsLoader = require('../new-settings-loader')
|
||||
const logger = require('../logger')
|
||||
|
||||
db.connect({ direct: true }).then(sco => {
|
||||
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')
|
||||
}).catch(console.error)
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
db.connect({ direct: true }).then(sco => {
|
||||
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')
|
||||
}).catch(console.error)
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
function machineAction(type, value) {
|
||||
const deviceId = value.deviceId
|
||||
|
|
@ -26,28 +30,41 @@ function machineAction (type, value) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -12,16 +12,20 @@ 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',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}())
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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 }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
@ -77,27 +89,31 @@ const loadRoutes = async () => {
|
|||
plugins: [
|
||||
devMode
|
||||
? ApolloServerPluginLandingPageLocalDefault()
|
||||
: ApolloServerPluginLandingPageDisabled()
|
||||
]
|
||||
: 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,7 +121,7 @@ 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() {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,17 @@
|
|||
{
|
||||
"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},
|
||||
"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", "Аҧсуа"],
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const languageRec = require('./data/languages.json')
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -30,19 +30,23 @@ function authDirectiveTransformer(schema, directiveName = 'auth') {
|
|||
// 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
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
UserInputError,
|
||||
}
|
||||
|
|
@ -12,9 +12,15 @@ 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]) => {
|
||||
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,
|
||||
|
|
@ -25,18 +31,18 @@ const generateAttestationOptions = (session, options) => {
|
|||
excludeCredentials: userDevices.map(dev => ({
|
||||
id: dev.data.credentialID,
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
||||
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
userVerification: 'discouraged',
|
||||
requireResidentKey: false
|
||||
}
|
||||
requireResidentKey: false,
|
||||
},
|
||||
})
|
||||
|
||||
session.webauthn = {
|
||||
attestation: {
|
||||
challenge: opts.challenge
|
||||
}
|
||||
challenge: opts.challenge,
|
||||
},
|
||||
}
|
||||
|
||||
return opts
|
||||
|
|
@ -44,23 +50,27 @@ const generateAttestationOptions = (session, options) => {
|
|||
}
|
||||
|
||||
const generateAssertionOptions = (session, options) => {
|
||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
||||
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']
|
||||
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||
})),
|
||||
userVerification: 'discouraged',
|
||||
rpID: options.domain
|
||||
rpID: options.domain,
|
||||
})
|
||||
|
||||
session.webauthn = {
|
||||
assertion: {
|
||||
challenge: opts.challenge
|
||||
}
|
||||
challenge: opts.challenge,
|
||||
},
|
||||
}
|
||||
|
||||
return opts
|
||||
|
|
@ -78,10 +88,9 @@ const validateAttestation = (session, options) => {
|
|||
credential: options.attestationResponse,
|
||||
expectedChallenge: `${expectedChallenge}`,
|
||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||
expectedRPID: options.domain
|
||||
})
|
||||
])
|
||||
.then(([user, verification]) => {
|
||||
expectedRPID: options.domain,
|
||||
}),
|
||||
]).then(([user, verification]) => {
|
||||
const { verified, attestationInfo } = verification
|
||||
|
||||
if (!(verified || attestationInfo)) {
|
||||
|
|
@ -89,21 +98,20 @@ const validateAttestation = (session, options) => {
|
|||
return false
|
||||
}
|
||||
|
||||
const {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
} = attestationInfo
|
||||
const { counter, credentialPublicKey, credentialID } = attestationInfo
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
||||
return credentials
|
||||
.getHardwareCredentialsByUserId(user.id)
|
||||
.then(userDevices => {
|
||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
||||
const existingDevice = userDevices.find(
|
||||
device => device.data.credentialID === credentialID,
|
||||
)
|
||||
|
||||
if (!existingDevice) {
|
||||
const newDevice = {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
credentialID,
|
||||
}
|
||||
credentials.createHardwareCredential(user.id, newDevice)
|
||||
}
|
||||
|
|
@ -115,22 +123,33 @@ const validateAttestation = (session, options) => {
|
|||
}
|
||||
|
||||
const validateAssertion = (session, options) => {
|
||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
||||
return userManagement
|
||||
.authenticateUser(options.username, options.password)
|
||||
.then(user => {
|
||||
const expectedChallenge = session.webauthn.assertion.challenge
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id).then(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
|
||||
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 {
|
||||
|
|
@ -139,7 +158,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)
|
||||
|
|
@ -154,9 +173,14 @@ const validateAssertion = (session, options) => {
|
|||
}
|
||||
|
||||
dbAuthenticator.data.counter = assertionInfo.newCounter
|
||||
return credentials.updateHardwareCredential(dbAuthenticator)
|
||||
return credentials
|
||||
.updateHardwareCredential(dbAuthenticator)
|
||||
.then(() => {
|
||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
||||
const finalUser = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
}
|
||||
session.user = finalUser
|
||||
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
||||
|
||||
|
|
@ -171,5 +195,5 @@ module.exports = {
|
|||
generateAttestationOptions,
|
||||
generateAssertionOptions,
|
||||
validateAttestation,
|
||||
validateAssertion
|
||||
validateAssertion,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,15 @@ 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]) => {
|
||||
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,
|
||||
|
|
@ -24,18 +30,18 @@ const generateAttestationOptions = (session, options) => {
|
|||
excludeCredentials: userDevices.map(dev => ({
|
||||
id: dev.data.credentialID,
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
||||
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
userVerification: 'discouraged',
|
||||
requireResidentKey: false
|
||||
}
|
||||
requireResidentKey: false,
|
||||
},
|
||||
})
|
||||
|
||||
session.webauthn = {
|
||||
attestation: {
|
||||
challenge: opts.challenge
|
||||
}
|
||||
challenge: opts.challenge,
|
||||
},
|
||||
}
|
||||
|
||||
return opts
|
||||
|
|
@ -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,10 +83,9 @@ const validateAttestation = (session, options) => {
|
|||
credential: options.attestationResponse,
|
||||
expectedChallenge: `${expectedChallenge}`,
|
||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||
expectedRPID: options.domain
|
||||
})
|
||||
])
|
||||
.then(([user, verification]) => {
|
||||
expectedRPID: options.domain,
|
||||
}),
|
||||
]).then(([user, verification]) => {
|
||||
const { verified, attestationInfo } = verification
|
||||
|
||||
if (!(verified || attestationInfo)) {
|
||||
|
|
@ -88,21 +93,20 @@ const validateAttestation = (session, options) => {
|
|||
return false
|
||||
}
|
||||
|
||||
const {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
} = attestationInfo
|
||||
const { counter, credentialPublicKey, credentialID } = attestationInfo
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
||||
return credentials
|
||||
.getHardwareCredentialsByUserId(user.id)
|
||||
.then(userDevices => {
|
||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
||||
const existingDevice = userDevices.find(
|
||||
device => device.data.credentialID === credentialID,
|
||||
)
|
||||
|
||||
if (!existingDevice) {
|
||||
const newDevice = {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
credentialID,
|
||||
}
|
||||
credentials.createHardwareCredential(user.id, newDevice)
|
||||
}
|
||||
|
|
@ -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,14 +159,16 @@ 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 }
|
||||
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
|
||||
|
||||
|
|
@ -170,5 +183,5 @@ module.exports = {
|
|||
generateAttestationOptions,
|
||||
generateAssertionOptions,
|
||||
validateAttestation,
|
||||
validateAssertion
|
||||
validateAssertion,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,10 +73,9 @@ const validateAttestation = (session, options) => {
|
|||
credential: options.attestationResponse,
|
||||
expectedChallenge: `${expectedChallenge}`,
|
||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||
expectedRPID: options.domain
|
||||
})
|
||||
])
|
||||
.then(([user, verification]) => {
|
||||
expectedRPID: options.domain,
|
||||
}),
|
||||
]).then(([user, verification]) => {
|
||||
const { verified, attestationInfo } = verification
|
||||
|
||||
if (!(verified || attestationInfo)) {
|
||||
|
|
@ -92,12 +91,15 @@ const validateAttestation = (session, options) => {
|
|||
credentialID,
|
||||
credentialType,
|
||||
userVerified,
|
||||
attestationObject
|
||||
attestationObject,
|
||||
} = attestationInfo
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
||||
return credentials
|
||||
.getHardwareCredentialsByUserId(user.id)
|
||||
.then(userDevices => {
|
||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
||||
const existingDevice = userDevices.find(
|
||||
device => device.data.credentialID === credentialID,
|
||||
)
|
||||
|
||||
if (!existingDevice) {
|
||||
const newDevice = {
|
||||
|
|
@ -108,7 +110,7 @@ const validateAttestation = (session, options) => {
|
|||
credentialID,
|
||||
credentialType,
|
||||
userVerified,
|
||||
attestationObject
|
||||
attestationObject,
|
||||
}
|
||||
credentials.createHardwareCredential(user.id, newDevice)
|
||||
}
|
||||
|
|
@ -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,10 +169,13 @@ 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 }
|
||||
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
|
||||
|
||||
|
|
@ -177,5 +189,5 @@ module.exports = {
|
|||
generateAttestationOptions,
|
||||
generateAssertionOptions,
|
||||
validateAttestation,
|
||||
validateAssertion
|
||||
validateAssertion,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,14 +63,12 @@ const initializeSession = (context, user, rememberMe) => {
|
|||
}
|
||||
|
||||
const executeProtectedAction = (code, id, context, action) => {
|
||||
return users.getUserById(id)
|
||||
.then(user => {
|
||||
return users.getUserById(id).then(user => {
|
||||
if (user.role !== 'superuser') {
|
||||
return action()
|
||||
}
|
||||
|
||||
return confirm2FA(code, context)
|
||||
.then(() => action())
|
||||
return confirm2FA(code, context).then(() => action())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -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,8 +116,7 @@ const confirm2FA = (token, context) => {
|
|||
|
||||
const validateRegisterLink = token => {
|
||||
if (!token) throw new authErrors.InvalidUrlError()
|
||||
return users.validateUserRegistrationToken(token)
|
||||
.then(r => {
|
||||
return users.validateUserRegistrationToken(token).then(r => {
|
||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||
return { username: r.username, role: r.role }
|
||||
})
|
||||
|
|
@ -112,8 +124,7 @@ const validateRegisterLink = token => {
|
|||
|
||||
const validateResetPasswordLink = token => {
|
||||
if (!token) throw new authErrors.InvalidUrlError()
|
||||
return users.validateAuthToken(token, 'reset_password')
|
||||
.then(r => {
|
||||
return users.validateAuthToken(token, 'reset_password').then(r => {
|
||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||
return { id: r.userID }
|
||||
})
|
||||
|
|
@ -121,17 +132,27 @@ const validateResetPasswordLink = token => {
|
|||
|
||||
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,10 +177,12 @@ const login = (username, password) => {
|
|||
}
|
||||
|
||||
const input2FA = (username, password, rememberMe, code, context) => {
|
||||
return authenticateUser(username, password)
|
||||
.then(user => {
|
||||
return authenticateUser(username, password).then(user => {
|
||||
const secret = user.twofa_code
|
||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||
const isCodeValid = otplib.authenticator.verify({
|
||||
token: code,
|
||||
secret: secret,
|
||||
})
|
||||
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||
|
||||
initializeSession(context, user, rememberMe)
|
||||
|
|
@ -164,10 +190,19 @@ const input2FA = (username, password, rememberMe, code, context) => {
|
|||
})
|
||||
}
|
||||
|
||||
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,8 +237,7 @@ const createReset2FAToken = (code, userID, context) => {
|
|||
}
|
||||
|
||||
const createRegisterToken = (username, role) => {
|
||||
return users.getUserByUsername(username)
|
||||
.then(user => {
|
||||
return users.getUserByUsername(username).then(user => {
|
||||
if (user) throw new authErrors.UserAlreadyExistsError()
|
||||
|
||||
return users.createUserRegistrationToken(username, role)
|
||||
|
|
@ -211,15 +245,15 @@ const createRegisterToken = (username, role) => {
|
|||
}
|
||||
|
||||
const register = (token, username, password, role) => {
|
||||
return users.getUserByUsername(username)
|
||||
.then(user => {
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ const { coins, currencies } = require('../../config')
|
|||
const resolver = {
|
||||
Query: {
|
||||
currencies: () => currencies,
|
||||
cryptoCurrencies: () => coins
|
||||
}
|
||||
cryptoCurrencies: () => coins,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolver
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ const funding = require('../../services/funding')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
funding: () => funding.getFunding()
|
||||
}
|
||||
funding: () => funding.getFunding(),
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const resolvers = [
|
|||
status,
|
||||
transaction,
|
||||
user,
|
||||
version
|
||||
version,
|
||||
]
|
||||
|
||||
module.exports = mergeResolvers(resolvers)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,24 +3,30 @@ const DataLoader = require('dataloader')
|
|||
const loyalty = require('../../../loyalty')
|
||||
const { getSlimCustomerByIdBatch } = require('../../../customers')
|
||||
|
||||
const customerLoader = new DataLoader(ids => {
|
||||
const customerLoader = new DataLoader(
|
||||
ids => {
|
||||
return getSlimCustomerByIdBatch(ids)
|
||||
}, { cache: false })
|
||||
},
|
||||
{ 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
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
const machineEventsLoader = new DataLoader(
|
||||
ids => {
|
||||
return machineEventsByIdBatch(ids)
|
||||
}, { cache: false })
|
||||
},
|
||||
{ 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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ const exchange = require('../../../exchange')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
getMarkets: () => exchange.getMarkets()
|
||||
}
|
||||
getMarkets: () => exchange.getMarkets(),
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const resolvers = {
|
|||
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
||||
const token = authentication.getToken(context)
|
||||
return sanctions.checkByUser(customerId, token)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ const supervisor = require('../../services/supervisor')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
uptime: () => supervisor.getAllProcessInfo()
|
||||
}
|
||||
uptime: () => supervisor.getAllProcessInfo(),
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ const serverVersion = require('../../../../package.json').version
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
serverVersion: () => serverVersion
|
||||
}
|
||||
serverVersion: () => serverVersion,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ const resolvers = require('./resolvers')
|
|||
|
||||
module.exports = {
|
||||
resolvers: resolvers,
|
||||
typeDefs: types
|
||||
typeDefs: types,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue