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 { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||||
|
import pluginJest from 'eslint-plugin-jest'
|
||||||
|
|
||||||
export default defineConfig([
|
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}'],
|
files: ['**/*.{js,mjs,cjs,jsx}'],
|
||||||
plugins: { js },
|
plugins: { js },
|
||||||
extends: ['js/recommended']
|
extends: ['js/recommended'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['packages/admin-ui/**/*.{js,mjs,jsx}'],
|
files: ['packages/admin-ui/**/*.{js,mjs,jsx}'],
|
||||||
|
|
@ -19,18 +27,18 @@ export default defineConfig([
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
process: 'readonly'
|
process: 'readonly',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['packages/server/**/*.{js,cjs}'],
|
files: ['packages/server/**/*.{js,cjs}'],
|
||||||
languageOptions: { sourceType: 'commonjs', globals: globals.node }
|
languageOptions: { sourceType: 'commonjs', globals: globals.node },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...pluginReact.configs.flat.recommended,
|
...pluginReact.configs.flat.recommended,
|
||||||
settings: { react: { version: 'detect' } },
|
settings: { react: { version: 'detect' } },
|
||||||
files: ['packages/admin-ui/**/*.{jsx,js}']
|
files: ['packages/admin-ui/**/*.{jsx,js}'],
|
||||||
},
|
},
|
||||||
{ ...reactCompiler.configs.recommended },
|
{ ...reactCompiler.configs.recommended },
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
|
|
@ -38,14 +46,29 @@ export default defineConfig([
|
||||||
files: ['**/*.json'],
|
files: ['**/*.json'],
|
||||||
plugins: { json },
|
plugins: { json },
|
||||||
language: 'json/json',
|
language: 'json/json',
|
||||||
extends: ['json/recommended']
|
extends: ['json/recommended'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
'react/no-unescaped-entities': '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/json": "^0.12.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"globals": "^16.1.0",
|
"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": {
|
"node_modules/@otplib/core": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
|
||||||
|
|
@ -7460,6 +7499,158 @@
|
||||||
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@vitejs/plugin-react-swc": {
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz",
|
||||||
|
|
@ -12177,6 +12368,32 @@
|
||||||
"eslint": ">=4.19.1"
|
"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": {
|
"node_modules/eslint-plugin-node": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
"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": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.5.tgz",
|
||||||
"integrity": "sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA=="
|
"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": {
|
"node_modules/fb-watchman": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==",
|
"integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/har-schema": {
|
"node_modules/har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|
@ -15331,6 +15589,7 @@
|
||||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"is-docker": "cli.js"
|
"is-docker": "cli.js"
|
||||||
},
|
},
|
||||||
|
|
@ -15722,6 +15981,7 @@
|
||||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -19561,6 +19821,16 @@
|
||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/merkle-lib": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz",
|
"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==",
|
"integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"growly": "^1.3.0",
|
"growly": "^1.3.0",
|
||||||
"is-wsl": "^2.2.0",
|
"is-wsl": "^2.2.0",
|
||||||
|
|
@ -20248,6 +20519,7 @@
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
|
|
@ -22993,6 +23265,17 @@
|
||||||
"node": ">= 4"
|
"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": {
|
"node_modules/rfdc": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
|
||||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|
@ -26364,6 +26648,19 @@
|
||||||
"semver": "bin/semver"
|
"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": {
|
"node_modules/ts-invariant": {
|
||||||
"version": "0.10.3",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"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": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.40",
|
"version": "1.0.40",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
"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/json": "^0.12.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-jest": "^28.11.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.1.0",
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ const CA_PATH = process.env.CA_PATH
|
||||||
const version = require('../package.json').version
|
const version = require('../package.json').version
|
||||||
logger.info('Version: %s', version)
|
logger.info('Version: %s', version)
|
||||||
|
|
||||||
function run () {
|
function run() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
let count = 0
|
let count = 0
|
||||||
let handler
|
let handler
|
||||||
|
|
||||||
|
|
@ -31,12 +31,11 @@ function run () {
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = () => {
|
const runner = () => {
|
||||||
settingsLoader.loadLatest()
|
settingsLoader
|
||||||
|
.loadLatest()
|
||||||
.then(settings => {
|
.then(settings => {
|
||||||
clearInterval(handler)
|
clearInterval(handler)
|
||||||
return loadSanctions(settings)
|
return loadSanctions(settings).then(startServer).then(resolve)
|
||||||
.then(startServer)
|
|
||||||
.then(resolve)
|
|
||||||
})
|
})
|
||||||
.catch(errorHandler)
|
.catch(errorHandler)
|
||||||
}
|
}
|
||||||
|
|
@ -46,23 +45,23 @@ function run () {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSanctions (settings) {
|
function loadSanctions(settings) {
|
||||||
return Promise.resolve()
|
return Promise.resolve().then(() => {
|
||||||
.then(() => {
|
const triggers = configManager.getTriggers(settings.config)
|
||||||
const triggers = configManager.getTriggers(settings.config)
|
const hasSanctions = complianceTriggers.hasSanctions(triggers)
|
||||||
const hasSanctions = complianceTriggers.hasSanctions(triggers)
|
|
||||||
|
|
||||||
if (!hasSanctions) return
|
if (!hasSanctions) return
|
||||||
|
|
||||||
logger.info('Loading sanctions DB...')
|
logger.info('Loading sanctions DB...')
|
||||||
return ofacUpdate.update()
|
return ofacUpdate
|
||||||
.then(() => logger.info('Sanctions DB updated'))
|
.update()
|
||||||
.then(ofac.load)
|
.then(() => logger.info('Sanctions DB updated'))
|
||||||
.then(() => logger.info('Sanctions DB loaded'))
|
.then(ofac.load)
|
||||||
})
|
.then(() => logger.info('Sanctions DB loaded'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startServer () {
|
async function startServer() {
|
||||||
const app = await loadRoutes()
|
const app = await loadRoutes()
|
||||||
|
|
||||||
poller.setup()
|
poller.setup()
|
||||||
|
|
@ -72,16 +71,14 @@ async function startServer () {
|
||||||
cert: fs.readFileSync(CERT_PATH),
|
cert: fs.readFileSync(CERT_PATH),
|
||||||
ca: fs.readFileSync(CA_PATH),
|
ca: fs.readFileSync(CA_PATH),
|
||||||
requestCert: true,
|
requestCert: true,
|
||||||
rejectUnauthorized: false
|
rejectUnauthorized: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = https.createServer(httpsServerOptions, app)
|
const server = https.createServer(httpsServerOptions, app)
|
||||||
|
|
||||||
const port = argv.port || 3000
|
const port = argv.port || 3000
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
await new Promise(resolve => server.listen({ port }, resolve))
|
||||||
server.listen({ port }, resolve),
|
|
||||||
)
|
|
||||||
logger.info(`lamassu-server listening on port ${port}`)
|
logger.info(`lamassu-server listening on port ${port}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const crypto = require('crypto')
|
||||||
const constants = require('./constants')
|
const constants = require('./constants')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
|
||||||
function createAuthToken (userID, type) {
|
function createAuthToken(userID, type) {
|
||||||
const token = crypto.randomBytes(32).toString('hex')
|
const token = crypto.randomBytes(32).toString('hex')
|
||||||
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '${constants.AUTH_TOKEN_EXPIRATION_TIME}' RETURNING *`
|
const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, $2, $3) ON CONFLICT (user_id, type) DO UPDATE SET token=$1, expire=now() + interval '${constants.AUTH_TOKEN_EXPIRATION_TIME}' RETURNING *`
|
||||||
|
|
||||||
|
|
@ -11,5 +11,5 @@ function createAuthToken (userID, type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createAuthToken
|
createAuthToken,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const sumService = require('@haensl/subset-sum')
|
const sumService = require('@haensl/subset-sum')
|
||||||
|
|
||||||
const BN = require('./bn')
|
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const cc = require('./coin-change')
|
const cc = require('./coin-change')
|
||||||
|
|
||||||
|
|
@ -11,7 +10,7 @@ const BILL_LIST_MODES = {
|
||||||
LOWEST_VALUE_FIRST: 2,
|
LOWEST_VALUE_FIRST: 2,
|
||||||
HIGHEST_VALUE_FIRST: 3,
|
HIGHEST_VALUE_FIRST: 3,
|
||||||
UNIT_ROUND_ROBIN: 4,
|
UNIT_ROUND_ROBIN: 4,
|
||||||
VALUE_ROUND_ROBIN: 5
|
VALUE_ROUND_ROBIN: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildBillList = (units, mode) => {
|
const buildBillList = (units, mode) => {
|
||||||
|
|
@ -23,7 +22,7 @@ const buildBillList = (units, mode) => {
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
_.reverse(units)
|
_.reverse(units),
|
||||||
)
|
)
|
||||||
case BILL_LIST_MODES.FIRST_UNIT_FIRST:
|
case BILL_LIST_MODES.FIRST_UNIT_FIRST:
|
||||||
return _.reduce(
|
return _.reduce(
|
||||||
|
|
@ -32,7 +31,7 @@ const buildBillList = (units, mode) => {
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
units
|
units,
|
||||||
)
|
)
|
||||||
case BILL_LIST_MODES.LOWEST_VALUE_FIRST:
|
case BILL_LIST_MODES.LOWEST_VALUE_FIRST:
|
||||||
return _.reduce(
|
return _.reduce(
|
||||||
|
|
@ -41,7 +40,7 @@ const buildBillList = (units, mode) => {
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
_.orderBy(['denomination'], ['asc'])(units)
|
_.orderBy(['denomination'], ['asc'])(units),
|
||||||
)
|
)
|
||||||
case BILL_LIST_MODES.HIGHEST_VALUE_FIRST:
|
case BILL_LIST_MODES.HIGHEST_VALUE_FIRST:
|
||||||
return _.reduce(
|
return _.reduce(
|
||||||
|
|
@ -50,58 +49,59 @@ const buildBillList = (units, mode) => {
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
_.orderBy(['denomination'], ['desc'])(units)
|
_.orderBy(['denomination'], ['desc'])(units),
|
||||||
|
)
|
||||||
|
case BILL_LIST_MODES.UNIT_ROUND_ROBIN: {
|
||||||
|
const amountOfBills = _.reduce(
|
||||||
|
(acc, value) => acc + value.count,
|
||||||
|
0,
|
||||||
|
units,
|
||||||
)
|
)
|
||||||
case BILL_LIST_MODES.UNIT_ROUND_ROBIN:
|
|
||||||
{
|
|
||||||
const amountOfBills = _.reduce(
|
|
||||||
(acc, value) => acc + value.count,
|
|
||||||
0,
|
|
||||||
units
|
|
||||||
)
|
|
||||||
|
|
||||||
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
|
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
|
||||||
const bills = []
|
const bills = []
|
||||||
|
|
||||||
for(let i = 0; i < amountOfBills; i++) {
|
for (let i = 0; i < amountOfBills; i++) {
|
||||||
const idx = i % _.size(_units)
|
const idx = i % _.size(_units)
|
||||||
if (_units[idx].count > 0) {
|
if (_units[idx].count > 0) {
|
||||||
bills.push(_units[idx].denomination)
|
bills.push(_units[idx].denomination)
|
||||||
_units[idx].count--
|
_units[idx].count--
|
||||||
}
|
|
||||||
|
|
||||||
if (_units[idx].count === 0) {
|
|
||||||
_units.splice(idx, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bills
|
if (_units[idx].count === 0) {
|
||||||
|
_units.splice(idx, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case BILL_LIST_MODES.VALUE_ROUND_ROBIN:
|
|
||||||
{
|
|
||||||
const amountOfBills = _.reduce(
|
|
||||||
(acc, value) => acc + value.count,
|
|
||||||
0,
|
|
||||||
units
|
|
||||||
)
|
|
||||||
|
|
||||||
const _units = _.flow([_.filter(it => it.count > 0), _.orderBy(['denomination'], ['asc'])])(_.cloneDeep(units))
|
return bills
|
||||||
const bills = []
|
}
|
||||||
|
case BILL_LIST_MODES.VALUE_ROUND_ROBIN: {
|
||||||
|
const amountOfBills = _.reduce(
|
||||||
|
(acc, value) => acc + value.count,
|
||||||
|
0,
|
||||||
|
units,
|
||||||
|
)
|
||||||
|
|
||||||
for(let i = 0; i < amountOfBills; i++) {
|
const _units = _.flow([
|
||||||
const idx = i % _.size(_units)
|
_.filter(it => it.count > 0),
|
||||||
if (_units[idx].count > 0) {
|
_.orderBy(['denomination'], ['asc']),
|
||||||
bills.push(_units[idx].denomination)
|
])(_.cloneDeep(units))
|
||||||
_units[idx].count--
|
const bills = []
|
||||||
}
|
|
||||||
|
|
||||||
if (_units[idx].count === 0) {
|
for (let i = 0; i < amountOfBills; i++) {
|
||||||
_units.splice(idx, 1)
|
const idx = i % _.size(_units)
|
||||||
}
|
if (_units[idx].count > 0) {
|
||||||
|
bills.push(_units[idx].denomination)
|
||||||
|
_units[idx].count--
|
||||||
}
|
}
|
||||||
|
|
||||||
return bills
|
if (_units[idx].count === 0) {
|
||||||
|
_units.splice(idx, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return bills
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid mode: ${mode}`)
|
throw new Error(`Invalid mode: ${mode}`)
|
||||||
}
|
}
|
||||||
|
|
@ -116,8 +116,10 @@ const getSolution_old = (units, amount, mode) => {
|
||||||
|
|
||||||
const solver = sumService.subsetSum(billList, amount.toNumber())
|
const solver = sumService.subsetSum(billList, amount.toNumber())
|
||||||
const solution = _.countBy(Math.floor, solver.next().value)
|
const solution = _.countBy(Math.floor, solver.next().value)
|
||||||
return Object.entries(solution)
|
return Object.entries(solution).map(([denomination, provisioned]) => [
|
||||||
.map(([denomination, provisioned]) => [_.toNumber(denomination), provisioned])
|
_.toNumber(denomination),
|
||||||
|
provisioned,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSolution = (units, amount) => {
|
const getSolution = (units, amount) => {
|
||||||
|
|
@ -128,28 +130,35 @@ const getSolution = (units, amount) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const solutionToOriginalUnits = (solution, units) => {
|
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)
|
const billsLeft = Object.fromEntries(solution)
|
||||||
return units.map(
|
return units.map(({ count, name, denomination }) => {
|
||||||
({ count, name, denomination }) => {
|
const provisioned = billsToAssign(count, billsLeft[denomination])
|
||||||
const provisioned = billsToAssign(count, billsLeft[denomination])
|
billsLeft[denomination] -= provisioned
|
||||||
billsLeft[denomination] -= provisioned
|
return { name, denomination, provisioned }
|
||||||
return { name, denomination, provisioned }
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChange(outCassettes, amount) {
|
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)
|
const cc_solution = getSolution(outCassettes, amount)
|
||||||
|
|
||||||
if (!cc.check(cc_solution, amount.toNumber())) {
|
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)
|
return solutionToOriginalUnits(ss_solution, outCassettes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!ss_solution !== !!cc_solution) {
|
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)
|
return solutionToOriginalUnits(ss_solution, outCassettes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const getBlacklist = () =>
|
||||||
db.any(
|
db.any(
|
||||||
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
|
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
|
||||||
FROM blacklist JOIN blacklist_messages
|
FROM blacklist JOIN blacklist_messages
|
||||||
ON blacklist.blacklist_message_id = blacklist_messages.id`
|
ON blacklist.blacklist_message_id = blacklist_messages.id`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const deleteFromBlacklist = address => {
|
const deleteFromBlacklist = address => {
|
||||||
|
|
@ -19,7 +19,9 @@ const deleteFromBlacklist = address => {
|
||||||
|
|
||||||
const isValidAddress = address => {
|
const isValidAddress = address => {
|
||||||
try {
|
try {
|
||||||
return !_.isEmpty(addressDetector.getSupportedCoinsForAddress(address).matches)
|
return !_.isEmpty(
|
||||||
|
addressDetector.getSupportedCoinsForAddress(address).matches,
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -29,24 +31,20 @@ const insertIntoBlacklist = address => {
|
||||||
if (!isValidAddress(address)) {
|
if (!isValidAddress(address)) {
|
||||||
return Promise.reject(new Error('Invalid address'))
|
return Promise.reject(new Error('Invalid address'))
|
||||||
}
|
}
|
||||||
return db
|
return db.none('INSERT INTO blacklist (address) VALUES ($1);', [address])
|
||||||
.none(
|
|
||||||
'INSERT INTO blacklist (address) VALUES ($1);',
|
|
||||||
[address]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function blocked (address) {
|
function blocked(address) {
|
||||||
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
|
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
|
||||||
return db.oneOrNone(sql, [address])
|
return db.oneOrNone(sql, [address])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessages () {
|
function getMessages() {
|
||||||
const sql = `SELECT * FROM blacklist_messages`
|
const sql = `SELECT * FROM blacklist_messages`
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function editBlacklistMessage (id, content) {
|
function editBlacklistMessage(id, content) {
|
||||||
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
||||||
return db.oneOrNone(sql, [content, id])
|
return db.oneOrNone(sql, [content, id])
|
||||||
}
|
}
|
||||||
|
|
@ -57,5 +55,5 @@ module.exports = {
|
||||||
deleteFromBlacklist,
|
deleteFromBlacklist,
|
||||||
insertIntoBlacklist,
|
insertIntoBlacklist,
|
||||||
getMessages,
|
getMessages,
|
||||||
editBlacklistMessage
|
editBlacklistMessage,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const _ = require('lodash/fp')
|
|
||||||
|
|
||||||
const { utils: coinUtils } = require('@lamassu/coins')
|
const { utils: coinUtils } = require('@lamassu/coins')
|
||||||
|
|
||||||
|
|
@ -13,9 +12,11 @@ const coinRec = coinUtils.getCryptoCurrency('BTC')
|
||||||
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
||||||
|
|
||||||
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp') : '/tmp'
|
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp') : '/tmp'
|
||||||
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
|
const usrBinDir = isDevMode()
|
||||||
|
? path.resolve(BLOCKCHAIN_DIR, 'bin')
|
||||||
|
: '/usr/local/bin'
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
!isDevMode() && common.firewall([coinRec.defaultPort])
|
!isDevMode() && common.firewall([coinRec.defaultPort])
|
||||||
const config = buildConfig()
|
const config = buildConfig()
|
||||||
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
||||||
|
|
@ -23,12 +24,17 @@ function setup (dataDir) {
|
||||||
!isDevMode() && common.writeSupervisorConfig(coinRec, cmd)
|
!isDevMode() && common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
||||||
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
||||||
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Bitcoin Core: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
|
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 -r ${tmpDir}/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm ${tmpDir}/bitcoin.tar.gz`)
|
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.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 {
|
} 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...`)
|
common.logger.info(`changetype already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Enabling bech32 change addresses in config file..`)
|
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...`)
|
common.logger.info(`listenonion already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Setting 'listenonion=0' in config file...`)
|
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...`)
|
common.logger.info(`fallbackfee already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
|
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...`)
|
common.logger.info(`rpcworkqueue already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
|
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()) {
|
if (isCurrentlyRunning && !isDevMode()) {
|
||||||
|
|
@ -81,7 +119,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Bitcoin Core is updated!')
|
common.logger.info('Bitcoin Core is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig () {
|
function buildConfig() {
|
||||||
return `rpcuser=lamassuserver
|
return `rpcuser=lamassuserver
|
||||||
rpcpassword=${common.randomPass()}
|
rpcpassword=${common.randomPass()}
|
||||||
${isDevMode() ? `regtest=1` : ``}
|
${isDevMode() ? `regtest=1` : ``}
|
||||||
|
|
@ -97,13 +135,15 @@ walletrbf=1
|
||||||
listenonion=0
|
listenonion=0
|
||||||
fallbackfee=0.00005
|
fallbackfee=0.00005
|
||||||
rpcworkqueue=2000
|
rpcworkqueue=2000
|
||||||
${isDevMode()
|
${
|
||||||
? `[regtest]
|
isDevMode()
|
||||||
|
? `[regtest]
|
||||||
rpcport=18333
|
rpcport=18333
|
||||||
bind=0.0.0.0:18332
|
bind=0.0.0.0:18332
|
||||||
${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}` : ``}`
|
||||||
: `rpcport=8333
|
: `rpcport=8333
|
||||||
bind=0.0.0.0:8332
|
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}` : ``}`
|
||||||
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
|
||||||
|
|
||||||
const coinRec = coinUtils.getCryptoCurrency('BCH')
|
const coinRec = coinUtils.getCryptoCurrency('BCH')
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const config = buildConfig()
|
const config = buildConfig()
|
||||||
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
||||||
|
|
@ -16,12 +16,17 @@ function setup (dataDir) {
|
||||||
common.writeSupervisorConfig(coinRec, cmd)
|
common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
|
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop bitcoincash`)
|
common.es(`sudo supervisorctl stop bitcoincash`)
|
||||||
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
|
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 -r /tmp/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm /tmp/bitcoincash.tar.gz`)
|
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...`)
|
common.logger.info(`listenonion already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Setting 'listenonion=0' in config file...`)
|
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) {
|
if (isCurrentlyRunning) {
|
||||||
|
|
@ -47,7 +58,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Bitcoin Cash is updated!')
|
common.logger.info('Bitcoin Cash is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig () {
|
function buildConfig() {
|
||||||
return `rpcuser=lamassuserver
|
return `rpcuser=lamassuserver
|
||||||
rpcpassword=${common.randomPass()}
|
rpcpassword=${common.randomPass()}
|
||||||
dbcache=500
|
dbcache=500
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const makeDir = require('make-dir')
|
||||||
|
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const logger = require('console-log-level')({level: 'info'})
|
const logger = require('console-log-level')({ level: 'info' })
|
||||||
|
|
||||||
const { isDevMode } = require('../environment-helper')
|
const { isDevMode } = require('../environment-helper')
|
||||||
|
|
||||||
|
|
@ -23,13 +23,15 @@ module.exports = {
|
||||||
isInstalledSoftware,
|
isInstalledSoftware,
|
||||||
writeFile,
|
writeFile,
|
||||||
getBinaries,
|
getBinaries,
|
||||||
isUpdateDependent
|
isUpdateDependent,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BINARIES = {
|
const BINARIES = {
|
||||||
BTC: {
|
BTC: {
|
||||||
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
|
defaultUrl:
|
||||||
defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
|
'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',
|
defaultDir: 'bitcoin-0.20.1/bin',
|
||||||
url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz',
|
url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'bitcoin-28.0/bin',
|
dir: 'bitcoin-28.0/bin',
|
||||||
|
|
@ -46,16 +48,20 @@ const BINARIES = {
|
||||||
urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d',
|
urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d',
|
||||||
},
|
},
|
||||||
DASH: {
|
DASH: {
|
||||||
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
|
defaultUrl:
|
||||||
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
|
'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',
|
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',
|
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',
|
dir: 'dashcore-21.1.1/bin',
|
||||||
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
|
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
defaultUrl:
|
||||||
defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
|
'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',
|
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',
|
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',
|
dir: 'litecoin-0.21.4/bin',
|
||||||
|
|
@ -64,38 +70,44 @@ const BINARIES = {
|
||||||
BCH: {
|
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',
|
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',
|
dir: 'bitcoin-cash-node-28.0.0/bin',
|
||||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']],
|
files: [
|
||||||
|
['bitcoind', 'bitcoincashd'],
|
||||||
|
['bitcoin-cli', 'bitcoincash-cli'],
|
||||||
|
],
|
||||||
urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158',
|
urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158',
|
||||||
},
|
},
|
||||||
XMR: {
|
XMR: {
|
||||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2',
|
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',
|
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',
|
urlHash: '51ba03928d189c1c11b5379cab17dd9ae8d2230056dc05c872d0f8dba4a87f1d',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const coinsUpdateDependent = ['BTC', 'LTC', 'DASH']
|
const coinsUpdateDependent = ['BTC', 'LTC', 'DASH']
|
||||||
|
|
||||||
function firewall (ports) {
|
function firewall(ports) {
|
||||||
if (!ports || ports.length === 0) throw new Error('No ports supplied')
|
if (!ports || ports.length === 0) throw new Error('No ports supplied')
|
||||||
const portsString = ports.join(',')
|
const portsString = ports.join(',')
|
||||||
es(`sudo ufw allow ${portsString}`)
|
es(`sudo ufw allow ${portsString}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomPass () {
|
function randomPass() {
|
||||||
return crypto.randomBytes(32).toString('hex')
|
return crypto.randomBytes(32).toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
function es (cmd) {
|
function es(cmd) {
|
||||||
const env = {HOME: os.userInfo().homedir}
|
const env = { HOME: os.userInfo().homedir }
|
||||||
const options = {encoding: 'utf8', env}
|
const options = { encoding: 'utf8', env }
|
||||||
const res = cp.execSync(cmd, options)
|
const res = cp.execSync(cmd, options)
|
||||||
logger.debug(res)
|
logger.debug(res)
|
||||||
return res.toString()
|
return res.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSupervisorConfig (cryptoCode, command, isWallet = false) {
|
function generateSupervisorConfig(cryptoCode, command, isWallet = false) {
|
||||||
return `[program:${cryptoCode}${isWallet ? `-wallet` : ``}]
|
return `[program:${cryptoCode}${isWallet ? `-wallet` : ``}]
|
||||||
command=nice ${command}
|
command=nice ${command}
|
||||||
autostart=true
|
autostart=true
|
||||||
|
|
@ -108,34 +120,46 @@ environment=HOME="/root"
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
|
function writeSupervisorConfig(coinRec, cmd, walletCmd = '') {
|
||||||
if (isInstalledSoftware(coinRec)) return
|
if (isInstalledSoftware(coinRec)) return
|
||||||
|
|
||||||
const blockchain = coinRec.code
|
const blockchain = coinRec.code
|
||||||
|
|
||||||
if (!_.isNil(coinRec.wallet)) {
|
if (!_.isNil(coinRec.wallet)) {
|
||||||
const supervisorConfigWallet = generateSupervisorConfig(blockchain, walletCmd, true)
|
const supervisorConfigWallet = generateSupervisorConfig(
|
||||||
writeFile(`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`, supervisorConfigWallet)
|
blockchain,
|
||||||
|
walletCmd,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
writeFile(
|
||||||
|
`/etc/supervisor/conf.d/${coinRec.code}-wallet.conf`,
|
||||||
|
supervisorConfigWallet,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const supervisorConfig = generateSupervisorConfig(blockchain, cmd)
|
const supervisorConfig = generateSupervisorConfig(blockchain, cmd)
|
||||||
writeFile(`/etc/supervisor/conf.d/${coinRec.code}.conf`, supervisorConfig)
|
writeFile(`/etc/supervisor/conf.d/${coinRec.code}.conf`, supervisorConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInstalledSoftware (coinRec) {
|
function isInstalledSoftware(coinRec) {
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
return fs.existsSync(`${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`)
|
return (
|
||||||
&& fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`)
|
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)
|
const walletInstalled = _.isNil(coinRec.wallet)
|
||||||
? true
|
? true
|
||||||
: fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.wallet.conf`)
|
: fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.wallet.conf`)
|
||||||
return nodeInstalled && walletInstalled
|
return nodeInstalled && walletInstalled
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchAndInstall (coinRec) {
|
function fetchAndInstall(coinRec) {
|
||||||
const requiresUpdate = isUpdateDependent(coinRec.cryptoCode)
|
const requiresUpdate = isUpdateDependent(coinRec.cryptoCode)
|
||||||
if (isInstalledSoftware(coinRec)) return
|
if (isInstalledSoftware(coinRec)) return
|
||||||
|
|
||||||
|
|
@ -149,12 +173,16 @@ function fetchAndInstall (coinRec) {
|
||||||
|
|
||||||
es(`wget -q ${url}`)
|
es(`wget -q ${url}`)
|
||||||
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
|
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
|
return
|
||||||
}
|
}
|
||||||
es(`tar -xf ${downloadFile}`)
|
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()) {
|
if (isDevMode()) {
|
||||||
makeDir.sync(usrBinDir)
|
makeDir.sync(usrBinDir)
|
||||||
|
|
@ -170,7 +198,7 @@ function fetchAndInstall (coinRec) {
|
||||||
}, binaries.files)
|
}, binaries.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFile (path, content) {
|
function writeFile(path, content) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(path, content)
|
fs.writeFileSync(path, content)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -183,12 +211,12 @@ function writeFile (path, content) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBinaries (coinCode) {
|
function getBinaries(coinCode) {
|
||||||
const binaries = BINARIES[coinCode]
|
const binaries = BINARIES[coinCode]
|
||||||
if (!binaries) throw new Error(`No such coin: ${coinCode}`)
|
if (!binaries) throw new Error(`No such coin: ${coinCode}`)
|
||||||
return binaries
|
return binaries
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUpdateDependent (coinCode) {
|
function isUpdateDependent(coinCode) {
|
||||||
return _.includes(coinCode, coinsUpdateDependent)
|
return _.includes(coinCode, coinsUpdateDependent)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
|
||||||
|
|
||||||
const coinRec = coinUtils.getCryptoCurrency('DASH')
|
const coinRec = coinUtils.getCryptoCurrency('DASH')
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const config = buildConfig()
|
const config = buildConfig()
|
||||||
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
||||||
|
|
@ -16,12 +16,17 @@ function setup (dataDir) {
|
||||||
common.writeSupervisorConfig(coinRec, cmd)
|
common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Dash Core. This may take a minute...')
|
common.logger.info('Updating Dash Core. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop dash`)
|
common.es(`sudo supervisorctl stop dash`)
|
||||||
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Dash Core: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
|
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 -r /tmp/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm /tmp/dash.tar.gz`)
|
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.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`)
|
||||||
common.es(`sed -i 's/enableprivatesend/enablecoinjoin/g' /mnt/blockchains/dash/dash.conf`)
|
common.es(
|
||||||
} else if (common.es(`grep "enablecoinjoin=" /mnt/blockchains/dash/dash.conf || true`)) {
|
`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...`)
|
common.logger.info(`enablecoinjoin already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Enabling CoinJoin in config file...`)
|
common.logger.info(`Enabling CoinJoin in config file...`)
|
||||||
common.es(`echo "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`)
|
common.es(`echo "\nenablecoinjoin=1" >> /mnt/blockchains/dash/dash.conf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (common.es(`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`)) {
|
if (
|
||||||
|
common.es(
|
||||||
|
`grep "privatesendautostart=" /mnt/blockchains/dash/dash.conf || true`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`)
|
common.logger.info(`Switching from 'PrivateSend' to 'CoinJoin'...`)
|
||||||
common.es(`sed -i 's/privatesendautostart/coinjoinautostart/g' /mnt/blockchains/dash/dash.conf`)
|
common.es(
|
||||||
} else if (common.es(`grep "coinjoinautostart=" /mnt/blockchains/dash/dash.conf || true`)) {
|
`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...`)
|
common.logger.info(`coinjoinautostart already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Enabling CoinJoin AutoStart in config file...`)
|
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`)) {
|
if (common.es(`grep "litemode=" /mnt/blockchains/dash/dash.conf || true`)) {
|
||||||
common.logger.info(`Switching from 'LiteMode' to 'DisableGovernance'...`)
|
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 {
|
} else {
|
||||||
common.es(`echo "\ndisablegovernance already defined, skipping..."`)
|
common.es(`echo "\ndisablegovernance already defined, skipping..."`)
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +91,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Dash Core is updated!')
|
common.logger.info('Dash Core is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig () {
|
function buildConfig() {
|
||||||
return `rpcuser=lamassuserver
|
return `rpcuser=lamassuserver
|
||||||
rpcpassword=${common.randomPass()}
|
rpcpassword=${common.randomPass()}
|
||||||
dbcache=500
|
dbcache=500
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,20 @@ const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
||||||
|
|
||||||
const MOUNT_POINT = BLOCKCHAIN_DIR
|
const MOUNT_POINT = BLOCKCHAIN_DIR
|
||||||
|
|
||||||
module.exports = {prepareVolume}
|
module.exports = { prepareVolume }
|
||||||
|
|
||||||
const logger = common.logger
|
const logger = common.logger
|
||||||
|
|
||||||
function isMounted () {
|
function isMounted() {
|
||||||
return fs.existsSync(MOUNT_POINT)
|
return fs.existsSync(MOUNT_POINT)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFormatted (volumePath) {
|
function isFormatted(volumePath) {
|
||||||
const res = common.es(`file --dereference -s ${volumePath}`).trim()
|
const res = common.es(`file --dereference -s ${volumePath}`).trim()
|
||||||
return res !== `${volumePath}: data`
|
return res !== `${volumePath}: data`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVolume (volumePath) {
|
function formatVolume(volumePath) {
|
||||||
if (isFormatted(volumePath)) {
|
if (isFormatted(volumePath)) {
|
||||||
logger.info('Volume is already formatted.')
|
logger.info('Volume is already formatted.')
|
||||||
return
|
return
|
||||||
|
|
@ -29,7 +29,7 @@ function formatVolume (volumePath) {
|
||||||
common.es(`sudo mkfs.ext4 ${volumePath}`)
|
common.es(`sudo mkfs.ext4 ${volumePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountVolume (volumePath) {
|
function mountVolume(volumePath) {
|
||||||
if (isMounted()) {
|
if (isMounted()) {
|
||||||
logger.info('Volume is already mounted.')
|
logger.info('Volume is already mounted.')
|
||||||
return
|
return
|
||||||
|
|
@ -38,10 +38,12 @@ function mountVolume (volumePath) {
|
||||||
logger.info('Mounting...')
|
logger.info('Mounting...')
|
||||||
common.es(`sudo mkdir -p ${MOUNT_POINT}`)
|
common.es(`sudo mkdir -p ${MOUNT_POINT}`)
|
||||||
common.es(`sudo mount -o discard,defaults ${volumePath} ${MOUNT_POINT}`)
|
common.es(`sudo mount -o discard,defaults ${volumePath} ${MOUNT_POINT}`)
|
||||||
common.es(`echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`)
|
common.es(
|
||||||
|
`echo ${volumePath} ${MOUNT_POINT} ext4 defaults,nofail,discard 0 0 | sudo tee -a /etc/fstab`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function locateVolume () {
|
function locateVolume() {
|
||||||
const res = common.es('ls /dev/disk/by-id/*')
|
const res = common.es('ls /dev/disk/by-id/*')
|
||||||
const lines = res.trim().split('\n')
|
const lines = res.trim().split('\n')
|
||||||
|
|
||||||
|
|
@ -58,7 +60,7 @@ function locateVolume () {
|
||||||
return lines[0].trim()
|
return lines[0].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareVolume () {
|
function prepareVolume() {
|
||||||
if (isMounted()) {
|
if (isMounted()) {
|
||||||
logger.info('Volume is already mounted.')
|
logger.info('Volume is already mounted.')
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ const common = require('./common')
|
||||||
|
|
||||||
module.exports = { setup, updateCore }
|
module.exports = { setup, updateCore }
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
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(`sudo supervisorctl stop ethereum`)
|
||||||
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
|
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!')
|
common.logger.info('Failed to update Geth: Package signature do not match!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +32,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Geth is updated!')
|
common.logger.info('Geth is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
const coinRec = coinUtils.getCryptoCurrency('ETH')
|
const coinRec = coinUtils.getCryptoCurrency('ETH')
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --http`
|
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --http`
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ const _ = require('lodash/fp')
|
||||||
const { utils: coinUtils } = require('@lamassu/coins')
|
const { utils: coinUtils } = require('@lamassu/coins')
|
||||||
const settingsLoader = require('../new-settings-loader')
|
const settingsLoader = require('../new-settings-loader')
|
||||||
const wallet = require('../wallet')
|
const wallet = require('../wallet')
|
||||||
const { isDevMode, isRemoteNode, isRemoteWallet } = require('../environment-helper')
|
const {
|
||||||
|
isDevMode,
|
||||||
|
isRemoteNode,
|
||||||
|
isRemoteWallet,
|
||||||
|
} = require('../environment-helper')
|
||||||
|
|
||||||
const common = require('./common')
|
const common = require('./common')
|
||||||
const doVolume = require('./do-volume')
|
const doVolume = require('./do-volume')
|
||||||
|
|
@ -24,35 +28,38 @@ const PLUGINS = {
|
||||||
BCH: require('./bitcoincash.js'),
|
BCH: require('./bitcoincash.js'),
|
||||||
DASH: require('./dash.js'),
|
DASH: require('./dash.js'),
|
||||||
LTC: require('./litecoin.js'),
|
LTC: require('./litecoin.js'),
|
||||||
XMR: require('./monero.js')
|
XMR: require('./monero.js'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isEnvironmentValid,
|
isEnvironmentValid,
|
||||||
run
|
run,
|
||||||
}
|
}
|
||||||
|
|
||||||
function installedVolumeFilePath (crypto) {
|
function installedVolumeFilePath(crypto) {
|
||||||
return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed')
|
return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed')
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInstalledVolume (crypto) {
|
function isInstalledVolume(crypto) {
|
||||||
return fs.existsSync(installedVolumeFilePath(crypto))
|
return fs.existsSync(installedVolumeFilePath(crypto))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInstalledSoftware (crypto) {
|
function isInstalledSoftware(crypto) {
|
||||||
return common.isInstalledSoftware(crypto)
|
return common.isInstalledSoftware(crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
function processCryptos (codes) {
|
function processCryptos(codes) {
|
||||||
if (_.isEmpty(codes)) {
|
if (_.isEmpty(codes)) {
|
||||||
logger.info('No cryptos selected. Exiting.')
|
logger.info('No cryptos selected. Exiting.')
|
||||||
process.exit(0)
|
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)
|
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
|
||||||
|
|
||||||
|
|
@ -89,22 +96,32 @@ function processCryptos (codes) {
|
||||||
logger.info('Installation complete.')
|
logger.info('Installation complete.')
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEnvironmentValid (crypto) {
|
function isEnvironmentValid(crypto) {
|
||||||
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_LOCATION`]))
|
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`]))
|
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))
|
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 (isRemoteNode(crypto) && !isRemoteWallet(crypto)) {
|
||||||
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_HOST`]))
|
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`]))
|
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))
|
if (_.isEmpty(process.env.BLOCKCHAIN_DIR))
|
||||||
throw new Error(`The environment variable for BLOCKCHAIN_DIR is not set!`)
|
throw new Error(`The environment variable for BLOCKCHAIN_DIR is not set!`)
|
||||||
|
|
@ -112,28 +129,39 @@ function isEnvironmentValid (crypto) {
|
||||||
|
|
||||||
if (isRemoteWallet(crypto)) {
|
if (isRemoteWallet(crypto)) {
|
||||||
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_HOST`]))
|
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`]))
|
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`]))
|
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`]))
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCrypto (crypto) {
|
function setupCrypto(crypto) {
|
||||||
logger.info(`Installing ${crypto.display}...`)
|
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)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +169,9 @@ function setupCrypto (crypto) {
|
||||||
makeDir.sync(cryptoDir)
|
makeDir.sync(cryptoDir)
|
||||||
const cryptoPlugin = plugin(crypto)
|
const cryptoPlugin = plugin(crypto)
|
||||||
const oldDir = process.cwd()
|
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)
|
makeDir.sync(tmpDir)
|
||||||
process.chdir(tmpDir)
|
process.chdir(tmpDir)
|
||||||
|
|
@ -157,62 +187,74 @@ function setupCrypto (crypto) {
|
||||||
process.chdir(oldDir)
|
process.chdir(oldDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCrypto (crypto) {
|
function updateCrypto(crypto) {
|
||||||
if (!common.isUpdateDependent(crypto.cryptoCode)) return
|
if (!common.isUpdateDependent(crypto.cryptoCode)) return
|
||||||
const cryptoPlugin = plugin(crypto)
|
const cryptoPlugin = plugin(crypto)
|
||||||
// TODO: we need to refactor the way we retrieve this status, p.e Monero uses two
|
// 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.
|
// services with specific names, so each coin should have its implementation.
|
||||||
// Currently, it's not a breaking change because only BTC is update dependent
|
// 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'])
|
const isCurrentlyRunning = _.includes(status, ['RUNNING', 'STARTING'])
|
||||||
cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), isCurrentlyRunning)
|
cryptoPlugin.updateCore(
|
||||||
|
common.getBinaries(crypto.cryptoCode),
|
||||||
|
isCurrentlyRunning,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function plugin (crypto) {
|
function plugin(crypto) {
|
||||||
const plugin = PLUGINS[crypto.cryptoCode]
|
const plugin = PLUGINS[crypto.cryptoCode]
|
||||||
if (!plugin) throw new Error(`No such plugin: ${crypto.cryptoCode}`)
|
if (!plugin) throw new Error(`No such plugin: ${crypto.cryptoCode}`)
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBlockchainSyncStatus (cryptoList) {
|
function getBlockchainSyncStatus(cryptoList) {
|
||||||
return settingsLoader.loadLatest()
|
return settingsLoader.loadLatest().then(settings => {
|
||||||
.then(settings => {
|
if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready')
|
||||||
if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready')
|
|
||||||
|
|
||||||
const blockchainStatuses = _.reduce((acc, value) => {
|
const blockchainStatuses = _.reduce(
|
||||||
const processStatus = common.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`).trim()
|
(acc, value) => {
|
||||||
return acc.then(a => {
|
const processStatus = common
|
||||||
if (processStatus === 'RUNNING') {
|
.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`)
|
||||||
return wallet.checkBlockchainStatus(settings, value.cryptoCode)
|
.trim()
|
||||||
.then(res => Promise.resolve({ ...a, [value.cryptoCode]: res }))
|
return acc.then(a => {
|
||||||
}
|
if (processStatus === 'RUNNING') {
|
||||||
return Promise.resolve({ ...a })
|
return wallet
|
||||||
})
|
.checkBlockchainStatus(settings, value.cryptoCode)
|
||||||
},
|
.then(res => Promise.resolve({ ...a, [value.cryptoCode]: res }))
|
||||||
Promise.resolve({}),
|
}
|
||||||
cryptoList
|
return Promise.resolve({ ...a })
|
||||||
)
|
})
|
||||||
|
},
|
||||||
|
Promise.resolve({}),
|
||||||
|
cryptoList,
|
||||||
|
)
|
||||||
|
|
||||||
return blockchainStatuses
|
return blockchainStatuses
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInstalled (crypto) {
|
function isInstalled(crypto) {
|
||||||
return isDevMode()
|
return isDevMode()
|
||||||
? isInstalledSoftware(crypto)
|
? isInstalledSoftware(crypto)
|
||||||
: isInstalledSoftware(crypto) && isInstalledVolume(crypto)
|
: isInstalledSoftware(crypto) && isInstalledVolume(crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDisabled (crypto) {
|
function isDisabled(crypto) {
|
||||||
switch (crypto.cryptoCode) {
|
switch (crypto.cryptoCode) {
|
||||||
case 'XMR':
|
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:
|
default:
|
||||||
return isInstalled(crypto) && 'Installed'
|
return isInstalled(crypto) && 'Installed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function run () {
|
function run() {
|
||||||
const choices = _.flow([
|
const choices = _.flow([
|
||||||
_.filter(c => !c.hideFromInstall),
|
_.filter(c => !c.hideFromInstall),
|
||||||
_.map(c => {
|
_.map(c => {
|
||||||
|
|
@ -220,47 +262,70 @@ function run () {
|
||||||
name: c.display,
|
name: c.display,
|
||||||
value: c.code,
|
value: c.code,
|
||||||
checked: isInstalled(c),
|
checked: isInstalled(c),
|
||||||
disabled: isDisabled(c)
|
disabled: isDisabled(c),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
])(cryptos)
|
])(cryptos)
|
||||||
|
|
||||||
const questions = []
|
const questions = []
|
||||||
|
|
||||||
const validateAnswers = async (answers) => {
|
const validateAnswers = async answers => {
|
||||||
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
|
if (_.size(answers) > 2)
|
||||||
|
return {
|
||||||
|
message: `Please insert a maximum of two coins to install.`,
|
||||||
|
isValid: false,
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
_.isEmpty(_.difference(['monero', 'zcash'], answers)) ||
|
_.isEmpty(_.difference(['monero', 'zcash'], answers)) ||
|
||||||
(_.includes('monero', answers) && isInstalled(_.find(it => it.code === 'zcash', cryptos))) ||
|
(_.includes('monero', answers) &&
|
||||||
(_.includes('zcash', answers) && isInstalled(_.find(it => it.code === 'monero', cryptos)))
|
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)
|
return getBlockchainSyncStatus(cryptos).then(blockchainStatuses => {
|
||||||
.then(blockchainStatuses => {
|
const result = _.reduce(
|
||||||
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
|
(acc, value) => ({
|
||||||
if (_.size(answers) + result.syncing > 2) {
|
...acc,
|
||||||
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 }
|
[value]: _.isNil(acc[value]) ? 1 : acc[value] + 1,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
_.values(blockchainStatuses),
|
||||||
|
)
|
||||||
|
if (_.size(answers) + result.syncing > 2) {
|
||||||
|
return {
|
||||||
|
message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`,
|
||||||
|
isValid: false,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.syncing > 2) {
|
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 }
|
return { message: null, isValid: true }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
questions.push({
|
questions.push({
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
name: 'crypto',
|
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.',
|
message:
|
||||||
choices
|
'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(answers => Promise.all([validateAnswers(answers.crypto), answers]))
|
||||||
.then(([res, answers]) => {
|
.then(([res, answers]) => {
|
||||||
if (res.isValid) {
|
if (res.isValid) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
|
||||||
|
|
||||||
const coinRec = coinUtils.getCryptoCurrency('LTC')
|
const coinRec = coinUtils.getCryptoCurrency('LTC')
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const config = buildConfig()
|
const config = buildConfig()
|
||||||
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
|
||||||
|
|
@ -16,12 +16,17 @@ function setup (dataDir) {
|
||||||
common.writeSupervisorConfig(coinRec, cmd)
|
common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop litecoin`)
|
common.es(`sudo supervisorctl stop litecoin`)
|
||||||
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Litecoin Core: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
|
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 -r /tmp/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm /tmp/litecoin.tar.gz`)
|
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...`)
|
common.logger.info(`changetype already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Enabling bech32 change addresses in config file..`)
|
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...`)
|
common.logger.info(`blockfilterindex already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Disabling blockfilterindex in config file..`)
|
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...`)
|
common.logger.info(`peerblockfilters already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Disabling peerblockfilters in config file..`)
|
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) {
|
if (isCurrentlyRunning) {
|
||||||
|
|
@ -60,7 +83,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Litecoin Core is updated!')
|
common.logger.info('Litecoin Core is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig () {
|
function buildConfig() {
|
||||||
return `rpcuser=lamassuserver
|
return `rpcuser=lamassuserver
|
||||||
rpcpassword=${common.randomPass()}
|
rpcpassword=${common.randomPass()}
|
||||||
dbcache=500
|
dbcache=500
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ module.exports = { setup, updateCore }
|
||||||
|
|
||||||
const coinRec = utils.getCryptoCurrency('XMR')
|
const coinRec = utils.getCryptoCurrency('XMR')
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
common.firewall([coinRec.defaultPort])
|
common.firewall([coinRec.defaultPort])
|
||||||
const auth = `lamassuserver:${common.randomPass()}`
|
const auth = `lamassuserver:${common.randomPass()}`
|
||||||
const config = buildConfig(auth)
|
const config = buildConfig(auth)
|
||||||
|
|
@ -18,19 +18,26 @@ function setup (dataDir) {
|
||||||
common.writeSupervisorConfig(coinRec, cmd, walletCmd)
|
common.writeSupervisorConfig(coinRec, cmd, walletCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Monero. This may take a minute...')
|
common.logger.info('Updating Monero. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
||||||
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Monero: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
|
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
common.es(`cp /tmp/${coinRec.dir}/monerod /usr/local/bin/monerod`)
|
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 -r /tmp/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm /tmp/monero.tar.gz`)
|
common.es(`rm /tmp/monero.tar.gz`)
|
||||||
|
|
||||||
|
|
@ -42,7 +49,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Monero is updated!')
|
common.logger.info('Monero is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig (auth) {
|
function buildConfig(auth) {
|
||||||
return `rpc-login=${auth}
|
return `rpc-login=${auth}
|
||||||
stagenet=0
|
stagenet=0
|
||||||
restricted-rpc=1
|
restricted-rpc=1
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,17 @@ module.exports = { setup, updateCore }
|
||||||
const es = common.es
|
const es = common.es
|
||||||
const logger = common.logger
|
const logger = common.logger
|
||||||
|
|
||||||
function updateCore (coinRec, isCurrentlyRunning) {
|
function updateCore(coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating your Zcash wallet. This may take a minute...')
|
common.logger.info('Updating your Zcash wallet. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop zcash`)
|
common.es(`sudo supervisorctl stop zcash`)
|
||||||
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
|
||||||
if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
if (
|
||||||
common.logger.info('Failed to update Zcash: Package signature do not match!')
|
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
|
return
|
||||||
}
|
}
|
||||||
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
|
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 -r /tmp/${coinRec.dir.replace('/bin', '')}`)
|
||||||
common.es(`rm /tmp/zcash.tar.gz`)
|
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...`)
|
common.logger.info(`walletrequirebackup already defined, skipping...`)
|
||||||
} else {
|
} else {
|
||||||
common.logger.info(`Setting 'walletrequirebackup=false' in config file...`)
|
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) {
|
if (isCurrentlyRunning) {
|
||||||
|
|
@ -39,7 +50,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Zcash is updated!')
|
common.logger.info('Zcash is updated!')
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup (dataDir) {
|
function setup(dataDir) {
|
||||||
es('sudo apt-get update')
|
es('sudo apt-get update')
|
||||||
es('sudo apt-get install libgomp1 -y')
|
es('sudo apt-get install libgomp1 -y')
|
||||||
const coinRec = coinUtils.getCryptoCurrency('ZEC')
|
const coinRec = coinUtils.getCryptoCurrency('ZEC')
|
||||||
|
|
@ -54,7 +65,7 @@ function setup (dataDir) {
|
||||||
common.writeSupervisorConfig(coinRec, cmd)
|
common.writeSupervisorConfig(coinRec, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfig () {
|
function buildConfig() {
|
||||||
return `mainnet=1
|
return `mainnet=1
|
||||||
addnode=mainnet.z.cash
|
addnode=mainnet.z.cash
|
||||||
rpcuser=lamassuserver
|
rpcuser=lamassuserver
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
const axios = require("axios");
|
const axios = require('axios')
|
||||||
|
|
||||||
const getSatBEstimateFee = () => {
|
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)
|
.then(r => r.data.hourFee)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSatBEstimateFees = () => {
|
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)
|
.then(r => r.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSatBEstimateFees,
|
getSatBEstimateFees,
|
||||||
getSatBEstimateFee
|
getSatBEstimateFee,
|
||||||
}
|
}
|
||||||
|
|
@ -8,63 +8,73 @@ const cashInLow = require('./cash-in-low')
|
||||||
|
|
||||||
module.exports = { atomic }
|
module.exports = { atomic }
|
||||||
|
|
||||||
function atomic (machineTx, pi) {
|
function atomic(machineTx) {
|
||||||
const TransactionMode = pgp.txMode.TransactionMode
|
const TransactionMode = pgp.txMode.TransactionMode
|
||||||
const isolationLevel = pgp.txMode.isolationLevel
|
const isolationLevel = pgp.txMode.isolationLevel
|
||||||
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
||||||
function transaction (t) {
|
function transaction(t) {
|
||||||
const sql = 'select * from cash_in_txs where id=$1'
|
const sql = 'select * from cash_in_txs where id=$1'
|
||||||
const sql2 = 'select * from bills where cash_in_txs_id=$1'
|
const sql2 = 'select * from bills where cash_in_txs_id=$1'
|
||||||
|
|
||||||
return t.oneOrNone(sql, [machineTx.id])
|
return t.oneOrNone(sql, [machineTx.id]).then(row => {
|
||||||
.then(row => {
|
if (row && row.tx_version >= machineTx.txVersion)
|
||||||
if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError({ txId: machineTx.id })
|
throw new E.StaleTxError({ txId: machineTx.id })
|
||||||
|
|
||||||
return t.any(sql2, [machineTx.id])
|
return t.any(sql2, [machineTx.id]).then(billRows => {
|
||||||
.then(billRows => {
|
const dbTx = cashInLow.toObj(row)
|
||||||
const dbTx = cashInLow.toObj(row)
|
|
||||||
|
|
||||||
return preProcess(dbTx, machineTx, pi)
|
return preProcess(dbTx, machineTx)
|
||||||
.then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx))
|
.then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx))
|
||||||
.then(r => {
|
.then(r => {
|
||||||
return insertNewBills(t, billRows, machineTx)
|
return insertNewBills(t, billRows, machineTx).then(newBills =>
|
||||||
.then(newBills => _.set('newBills', newBills, r))
|
_.set('newBills', newBills, r),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return db.tx({ mode }, transaction)
|
return db.tx({ mode }, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNewBills (t, billRows, machineTx) {
|
function insertNewBills(t, billRows, machineTx) {
|
||||||
const bills = pullNewBills(billRows, machineTx)
|
const bills = pullNewBills(billRows, machineTx)
|
||||||
if (_.isEmpty(bills)) return Promise.resolve([])
|
if (_.isEmpty(bills)) return Promise.resolve([])
|
||||||
|
|
||||||
const dbBills = _.map(cashInLow.massage, bills)
|
const dbBills = _.map(cashInLow.massage, bills)
|
||||||
const billsByDestination = _.countBy(_.get(['destination_unit']) ,dbBills)
|
const billsByDestination = _.countBy(_.get(['destination_unit']), dbBills)
|
||||||
|
|
||||||
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time', 'destination_unit']
|
const columns = [
|
||||||
|
'id',
|
||||||
|
'fiat',
|
||||||
|
'fiat_code',
|
||||||
|
'crypto_code',
|
||||||
|
'cash_in_fee',
|
||||||
|
'cash_in_txs_id',
|
||||||
|
'device_time',
|
||||||
|
'destination_unit',
|
||||||
|
]
|
||||||
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
||||||
const deviceID = machineTx.deviceId
|
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
|
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`
|
where device_id = $1`
|
||||||
|
|
||||||
return t.none(sql2, [
|
return t
|
||||||
deviceID,
|
.none(sql2, [
|
||||||
_.defaultTo(0, billsByDestination.recycler1),
|
deviceID,
|
||||||
_.defaultTo(0, billsByDestination.recycler2),
|
_.defaultTo(0, billsByDestination.recycler1),
|
||||||
_.defaultTo(0, billsByDestination.recycler3),
|
_.defaultTo(0, billsByDestination.recycler2),
|
||||||
_.defaultTo(0, billsByDestination.recycler4),
|
_.defaultTo(0, billsByDestination.recycler3),
|
||||||
_.defaultTo(0, billsByDestination.recycler5),
|
_.defaultTo(0, billsByDestination.recycler4),
|
||||||
_.defaultTo(0, billsByDestination.recycler6)
|
_.defaultTo(0, billsByDestination.recycler5),
|
||||||
])
|
_.defaultTo(0, billsByDestination.recycler6),
|
||||||
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return t.none(sql)
|
return t.none(sql)
|
||||||
})
|
})
|
||||||
.then(() => bills)
|
.then(() => bills)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pullNewBills (billRows, machineTx) {
|
function pullNewBills(billRows, machineTx) {
|
||||||
if (_.isEmpty(machineTx.bills)) return []
|
if (_.isEmpty(machineTx.bills)) return []
|
||||||
|
|
||||||
const toBill = _.mapKeys(_.camelCase)
|
const toBill = _.mapKeys(_.camelCase)
|
||||||
|
|
@ -73,7 +83,7 @@ function pullNewBills (billRows, machineTx) {
|
||||||
return _.differenceBy(_.get('id'), machineTx.bills, bills)
|
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,
|
// Note: The way this works is if we're clear to send,
|
||||||
// we mark the transaction as sendPending.
|
// we mark the transaction as sendPending.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,40 @@ const E = require('../error')
|
||||||
|
|
||||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
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 massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||||
|
|
||||||
const massage = _.flow(_.omit(massageFields),
|
const massage = _.flow(
|
||||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
_.omit(massageFields),
|
||||||
|
convertBigNumFields,
|
||||||
|
_.mapKeys(_.snakeCase),
|
||||||
|
)
|
||||||
|
|
||||||
const massageUpdates = _.flow(_.omit(massageUpdateFields),
|
const massageUpdates = _.flow(
|
||||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
_.omit(massageUpdateFields),
|
||||||
|
convertBigNumFields,
|
||||||
|
_.mapKeys(_.snakeCase),
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
|
module.exports = { toObj, upsert, insert, update, massage, isClearToSend }
|
||||||
|
|
||||||
function convertBigNumFields (obj) {
|
function convertBigNumFields(obj) {
|
||||||
const convert = value =>
|
const convert = value =>
|
||||||
value && BN.isBigNumber(value)
|
value && BN.isBigNumber(value) ? value.toString() : value
|
||||||
? value.toString()
|
|
||||||
: value
|
|
||||||
return _.mapValues(convert, obj)
|
return _.mapValues(convert, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toObj (row) {
|
function toObj(row) {
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
|
||||||
const keys = _.keys(row)
|
const keys = _.keys(row)
|
||||||
|
|
@ -35,7 +49,15 @@ function toObj (row) {
|
||||||
|
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const objKey = _.camelCase(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])
|
newObj[objKey] = new BN(row[key])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -48,35 +70,35 @@ function toObj (row) {
|
||||||
return newObj
|
return newObj
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert (t, dbTx, preProcessedTx) {
|
function upsert(t, dbTx, preProcessedTx) {
|
||||||
if (!dbTx) {
|
if (!dbTx) {
|
||||||
return insert(t, preProcessedTx)
|
return insert(t, preProcessedTx).then(tx => ({ dbTx, tx }))
|
||||||
.then(tx => ({dbTx, tx}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return update(t, dbTx, diff(dbTx, preProcessedTx))
|
return update(t, dbTx, diff(dbTx, preProcessedTx)).then(tx => ({ dbTx, tx }))
|
||||||
.then(tx => ({dbTx, tx}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert (t, tx) {
|
function insert(t, tx) {
|
||||||
const dbTx = massage(tx)
|
const dbTx = massage(tx)
|
||||||
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
|
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
|
||||||
return t.one(sql)
|
return t.one(sql).then(toObj)
|
||||||
.then(toObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (t, tx, changes) {
|
function update(t, tx, changes) {
|
||||||
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
||||||
|
|
||||||
const dbChanges = isFinalTxStage(changes) ? massage(changes) : massageUpdates(changes)
|
const dbChanges = isFinalTxStage(changes)
|
||||||
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
|
? massage(changes)
|
||||||
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
|
: 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)
|
return t.one(sql).then(toObj)
|
||||||
.then(toObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function diff (oldTx, newTx) {
|
function diff(oldTx, newTx) {
|
||||||
let updatedTx = {}
|
let updatedTx = {}
|
||||||
|
|
||||||
if (!oldTx) throw new Error('oldTx must not be null')
|
if (!oldTx) throw new Error('oldTx must not be null')
|
||||||
|
|
@ -89,10 +111,15 @@ function diff (oldTx, newTx) {
|
||||||
if (_.isEqualWith(nilEqual, oldField, newField)) return
|
if (_.isEqualWith(nilEqual, oldField, newField)) return
|
||||||
|
|
||||||
if (!ensureRatchet(oldField, newField, fieldKey)) {
|
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('Old tx: %j', oldTx)
|
||||||
logger.warn('New tx: %j', newTx)
|
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
|
updatedTx[fieldKey] = newField
|
||||||
|
|
@ -101,12 +128,29 @@ function diff (oldTx, newTx) {
|
||||||
return updatedTx
|
return updatedTx
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureRatchet (oldField, newField, fieldKey) {
|
function ensureRatchet(oldField, newField, fieldKey) {
|
||||||
const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion', 'batched', 'discount']
|
const monotonic = [
|
||||||
const free = ['sendPending', 'error', 'errorCode', 'customerId', 'discountSource']
|
'cryptoAtoms',
|
||||||
|
'fiat',
|
||||||
|
'send',
|
||||||
|
'sendConfirmed',
|
||||||
|
'operatorCompleted',
|
||||||
|
'timedout',
|
||||||
|
'txVersion',
|
||||||
|
'batched',
|
||||||
|
'discount',
|
||||||
|
]
|
||||||
|
const free = [
|
||||||
|
'sendPending',
|
||||||
|
'error',
|
||||||
|
'errorCode',
|
||||||
|
'customerId',
|
||||||
|
'discountSource',
|
||||||
|
]
|
||||||
|
|
||||||
if (_.isNil(oldField)) return true
|
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 (_.includes(fieldKey, free)) {
|
||||||
if (_.isNil(newField)) return false
|
if (_.isNil(newField)) return false
|
||||||
|
|
@ -114,13 +158,14 @@ function ensureRatchet (oldField, newField, fieldKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.isNil(newField)) return false
|
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
|
if (oldField.toString() === newField.toString()) return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMonotonic (oldField, newField, fieldKey) {
|
function isMonotonic(oldField, newField, fieldKey) {
|
||||||
if (_.isNil(newField)) return false
|
if (_.isNil(newField)) return false
|
||||||
if (_.isBoolean(oldField)) return oldField === newField || !oldField
|
if (_.isBoolean(oldField)) return oldField === newField || !oldField
|
||||||
if (BN.isBigNumber(oldField)) return oldField.lte(newField)
|
if (BN.isBigNumber(oldField)) return oldField.lte(newField)
|
||||||
|
|
@ -129,20 +174,22 @@ function isMonotonic (oldField, newField, fieldKey) {
|
||||||
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
|
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function nilEqual (a, b) {
|
function nilEqual(a, b) {
|
||||||
if (_.isNil(a) && _.isNil(b)) return true
|
if (_.isNil(a) && _.isNil(b)) return true
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClearToSend (oldTx, newTx) {
|
function isClearToSend(oldTx, newTx) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
return (newTx.send || newTx.batched) &&
|
return (
|
||||||
|
(newTx.send || newTx.batched) &&
|
||||||
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
|
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
|
||||||
(newTx.created > now - PENDING_INTERVAL_MS)
|
newTx.created > now - PENDING_INTERVAL_MS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFinalTxStage (txChanges) {
|
function isFinalTxStage(txChanges) {
|
||||||
return txChanges.send || txChanges.batched
|
return txChanges.send || txChanges.batched
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,93 +25,114 @@ case
|
||||||
else 'Pending'
|
else 'Pending'
|
||||||
end`
|
end`
|
||||||
|
|
||||||
module.exports = { post, monitorPending, cancel, PENDING_INTERVAL, TRANSACTION_STATES }
|
module.exports = {
|
||||||
|
post,
|
||||||
|
monitorPending,
|
||||||
|
cancel,
|
||||||
|
PENDING_INTERVAL,
|
||||||
|
TRANSACTION_STATES,
|
||||||
|
}
|
||||||
|
|
||||||
function post (machineTx, pi) {
|
function post(machineTx, pi) {
|
||||||
logger.silly('Updating cashin tx:', machineTx)
|
logger.silly('Updating cashin tx:', machineTx)
|
||||||
return cashInAtomic.atomic(machineTx, pi)
|
return cashInAtomic.atomic(machineTx).then(r => {
|
||||||
.then(r => {
|
const updatedTx = r.tx
|
||||||
const updatedTx = r.tx
|
let addressReuse = false
|
||||||
let addressReuse = false
|
|
||||||
|
|
||||||
const promises = [settingsLoader.loadLatestConfig()]
|
const promises = [settingsLoader.loadLatestConfig()]
|
||||||
|
|
||||||
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||||
if (isFirstPost) {
|
if (isFirstPost) {
|
||||||
promises.push(
|
promises.push(
|
||||||
checkForBlacklisted(updatedTx),
|
checkForBlacklisted(updatedTx),
|
||||||
doesTxReuseAddress(updatedTx),
|
doesTxReuseAddress(updatedTx),
|
||||||
getWalletScore(updatedTx, pi)
|
getWalletScore(updatedTx, pi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises).then(
|
||||||
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
([
|
||||||
const { rejectAddressReuse } = configManager.getCompliance(config)
|
config,
|
||||||
const isBlacklisted = !!blacklisted
|
blacklisted = false,
|
||||||
|
isReusedAddress = false,
|
||||||
|
walletScore = null,
|
||||||
|
]) => {
|
||||||
|
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||||
|
const isBlacklisted = !!blacklisted
|
||||||
|
|
||||||
if (isBlacklisted) {
|
if (isBlacklisted) {
|
||||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
||||||
} else if (isReusedAddress && rejectAddressReuse) {
|
} else if (isReusedAddress && rejectAddressReuse) {
|
||||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
||||||
addressReuse = true
|
addressReuse = true
|
||||||
}
|
}
|
||||||
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
||||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
.then(changes =>
|
||||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
_.set(
|
||||||
.then(_.flow(
|
'walletScore',
|
||||||
|
_.isNil(walletScore) ? null : walletScore.score,
|
||||||
|
changes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||||
|
.then(
|
||||||
|
_.flow(
|
||||||
_.set('bills', machineTx.bills),
|
_.set('bills', machineTx.bills),
|
||||||
_.set('blacklisted', isBlacklisted),
|
_.set('blacklisted', isBlacklisted),
|
||||||
_.set('blacklistMessage', blacklisted?.content),
|
_.set('blacklistMessage', blacklisted?.content),
|
||||||
_.set('addressReuse', addressReuse),
|
_.set('addressReuse', addressReuse),
|
||||||
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
|
_.set(
|
||||||
))
|
'validWalletScore',
|
||||||
})
|
_.isNil(walletScore) || walletScore.isValid,
|
||||||
})
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerTrades (pi, r) {
|
function registerTrades(pi, r) {
|
||||||
_.forEach(bill => pi.buy(bill, r.tx), r.newBills)
|
_.forEach(bill => pi.buy(bill, r.tx), r.newBills)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logAction (rec, tx) {
|
function logAction(rec, tx) {
|
||||||
const action = {
|
const action = {
|
||||||
tx_id: tx.id,
|
tx_id: tx.id,
|
||||||
action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
|
action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
|
||||||
error: rec.error,
|
error: rec.error,
|
||||||
error_code: rec.errorCode,
|
error_code: rec.errorCode,
|
||||||
tx_hash: rec.txHash
|
tx_hash: rec.txHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
const sql = pgp.helpers.insert(action, null, 'cash_in_actions')
|
const sql = pgp.helpers.insert(action, null, 'cash_in_actions')
|
||||||
|
|
||||||
return db.none(sql)
|
return db.none(sql).then(_.constant(rec))
|
||||||
.then(_.constant(rec))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logActionById (action, _rec, txId) {
|
function logActionById(action, _rec, txId) {
|
||||||
const rec = _.assign(_rec, { action, tx_id: txId })
|
const rec = _.assign(_rec, { action, tx_id: txId })
|
||||||
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
|
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
|
||||||
|
|
||||||
return db.none(sql)
|
return db.none(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForBlacklisted (tx) {
|
function checkForBlacklisted(tx) {
|
||||||
return blacklist.blocked(tx.toAddress)
|
return blacklist.blocked(tx.toAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
function postProcess(r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
if (addressReuse) {
|
if (addressReuse) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
operatorCompleted: true,
|
operatorCompleted: true,
|
||||||
error: 'Address Reused'
|
error: 'Address Reused',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBlacklisted) {
|
if (isBlacklisted) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
operatorCompleted: true,
|
operatorCompleted: true,
|
||||||
error: 'Blacklisted Address'
|
error: 'Blacklisted Address',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +141,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
walletScore: walletScore.score,
|
walletScore: walletScore.score,
|
||||||
operatorCompleted: true,
|
operatorCompleted: true,
|
||||||
error: 'Chain analysis score is above defined threshold',
|
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({})
|
if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({})
|
||||||
|
|
||||||
return pi.sendCoins(r.tx)
|
return pi
|
||||||
|
.sendCoins(r.tx)
|
||||||
.then(txObj => {
|
.then(txObj => {
|
||||||
if (txObj.batched) {
|
if (txObj.batched) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -136,7 +158,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
batchTime: 'now()^',
|
batchTime: 'now()^',
|
||||||
sendPending: true,
|
sendPending: true,
|
||||||
error: null,
|
error: null,
|
||||||
errorCode: null
|
errorCode: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +169,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
sendTime: 'now()^',
|
sendTime: 'now()^',
|
||||||
sendPending: false,
|
sendPending: false,
|
||||||
error: null,
|
error: null,
|
||||||
errorCode: null
|
errorCode: null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
@ -161,17 +183,18 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
sendTime: 'now()^',
|
sendTime: 'now()^',
|
||||||
error: err.message,
|
error: err.message,
|
||||||
errorCode: err.name,
|
errorCode: err.name,
|
||||||
sendPending: true
|
sendPending: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(sendRec => {
|
.then(sendRec => {
|
||||||
pi.notifyOperator(r.tx, sendRec)
|
pi.notifyOperator(r.tx, sendRec).catch(err =>
|
||||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
logger.error('Failure sending transaction notification', err),
|
||||||
|
)
|
||||||
return logAction(sendRec, r.tx)
|
return logAction(sendRec, r.tx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function doesTxReuseAddress (tx) {
|
function doesTxReuseAddress(tx) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT DISTINCT to_address FROM (
|
SELECT DISTINCT to_address FROM (
|
||||||
|
|
@ -181,15 +204,14 @@ function doesTxReuseAddress (tx) {
|
||||||
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore(tx, pi) {
|
||||||
return pi.isWalletScoringEnabled(tx)
|
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
|
||||||
.then(isEnabled => {
|
if (!isEnabled) return null
|
||||||
if (!isEnabled) return null
|
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
|
||||||
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorPending (settings) {
|
function monitorPending(settings) {
|
||||||
const sql = `select * from cash_in_txs
|
const sql = `select * from cash_in_txs
|
||||||
where created > now() - interval $1
|
where created > now() - interval $1
|
||||||
and send
|
and send
|
||||||
|
|
@ -203,27 +225,29 @@ function monitorPending (settings) {
|
||||||
const tx = cashInLow.toObj(row)
|
const tx = cashInLow.toObj(row)
|
||||||
const pi = plugins(settings, tx.deviceId)
|
const pi = plugins(settings, tx.deviceId)
|
||||||
|
|
||||||
return post(tx, pi)
|
return post(tx, pi).catch(logger.error)
|
||||||
.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)))
|
.then(rows => pEachSeries(rows, row => processPending(row)))
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel (txId) {
|
function cancel(txId) {
|
||||||
const updateRec = {
|
const updateRec = {
|
||||||
error: 'Operator cancel',
|
error: 'Operator cancel',
|
||||||
error_code: 'operatorCancel',
|
error_code: 'operatorCancel',
|
||||||
operator_completed: true,
|
operator_completed: true,
|
||||||
batch_id: null
|
batch_id: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return pgp.helpers.update(updateRec, null, 'cash_in_txs') +
|
return (
|
||||||
pgp.as.format(' where id=$1', [txId])
|
pgp.helpers.update(updateRec, null, 'cash_in_txs') +
|
||||||
|
pgp.as.format(' where id=$1', [txId])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.then(sql => db.result(sql, false))
|
.then(sql => db.result(sql, false))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,63 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
module.exports = {logDispense, logActionById, logAction, logError}
|
module.exports = { logDispense, logActionById, logAction, logError }
|
||||||
|
|
||||||
function logDispense (t, tx) {
|
function logDispense(t, tx) {
|
||||||
const baseRec = {error: tx.error, error_code: tx.errorCode}
|
const baseRec = { error: tx.error, error_code: tx.errorCode }
|
||||||
const rec = _.merge(mapDispense(tx), baseRec)
|
const rec = _.merge(mapDispense(tx), baseRec)
|
||||||
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
|
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
|
||||||
return logAction(t, action, rec, tx)
|
return logAction(t, action, rec, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logActionById (t, action, _rec, txId) {
|
function logActionById(t, action, _rec, txId) {
|
||||||
const rec = _.assign(_rec, {action, tx_id: txId, redeem: false})
|
const rec = _.assign(_rec, { action, tx_id: txId, redeem: false })
|
||||||
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
|
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
|
||||||
|
|
||||||
return t.none(sql)
|
return t.none(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logAction (t, action, _rec, tx) {
|
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')
|
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
|
||||||
|
|
||||||
return t.none(sql)
|
return t.none(sql).then(_.constant(tx))
|
||||||
.then(_.constant(tx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logError (t, action, err, tx) {
|
function logError(t, action, err, tx) {
|
||||||
return logAction(t, action, {
|
return logAction(
|
||||||
error: err.message,
|
t,
|
||||||
error_code: err.name
|
action,
|
||||||
}, tx)
|
{
|
||||||
|
error: err.message,
|
||||||
|
error_code: err.name,
|
||||||
|
},
|
||||||
|
tx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispense (tx) {
|
function mapDispense(tx) {
|
||||||
const bills = tx.bills
|
const bills = tx.bills
|
||||||
|
|
||||||
if (_.isEmpty(bills)) return {}
|
if (_.isEmpty(bills)) return {}
|
||||||
|
|
||||||
const res = {}
|
const res = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(
|
||||||
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
|
it => {
|
||||||
res[`provisioned_${suffix}`] = bills[it].provisioned
|
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
|
||||||
res[`denomination_${suffix}`] = bills[it].denomination
|
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
res[`dispensed_${suffix}`] = bills[it].dispensed
|
res[`denomination_${suffix}`] = bills[it].denomination
|
||||||
res[`rejected_${suffix}`] = bills[it].rejected
|
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
res[`rejected_${suffix}`] = bills[it].rejected
|
||||||
|
},
|
||||||
|
_.times(_.identity(), _.size(bills)),
|
||||||
|
)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,170 +13,208 @@ const toObj = helper.toObj
|
||||||
|
|
||||||
module.exports = { atomic }
|
module.exports = { atomic }
|
||||||
|
|
||||||
function atomic (tx, pi, fromClient) {
|
function atomic(tx, pi, fromClient) {
|
||||||
const TransactionMode = pgp.txMode.TransactionMode
|
const TransactionMode = pgp.txMode.TransactionMode
|
||||||
const isolationLevel = pgp.txMode.isolationLevel
|
const isolationLevel = pgp.txMode.isolationLevel
|
||||||
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
||||||
function transaction (t) {
|
function transaction(t) {
|
||||||
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE'
|
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(toObj)
|
||||||
.then(oldTx => {
|
.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 })
|
if (isStale) throw new E.StaleTxError({ txId: tx.id })
|
||||||
|
|
||||||
// Server doesn't bump version, so we just prevent from version being older.
|
// Server doesn't bump version, so we just prevent from version being older.
|
||||||
const isStaleFromServer = !fromClient && oldTx && (oldTx.txVersion > tx.txVersion)
|
const isStaleFromServer =
|
||||||
if (isStaleFromServer) throw new Error('Stale Error: server triggered', tx.id)
|
!fromClient && oldTx && oldTx.txVersion > tx.txVersion
|
||||||
|
if (isStaleFromServer)
|
||||||
|
throw new Error('Stale Error: server triggered', tx.id)
|
||||||
|
|
||||||
return preProcess(t, oldTx, tx, pi)
|
return preProcess(t, oldTx, tx, pi).then(preProcessedTx =>
|
||||||
.then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx))
|
cashOutLow.upsert(t, oldTx, preProcessedTx),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return db.tx({ mode }, transaction)
|
return db.tx({ mode }, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
function preProcess (t, oldTx, newTx, pi) {
|
function preProcess(t, oldTx, newTx, pi) {
|
||||||
if (!oldTx) {
|
if (!oldTx) {
|
||||||
return pi.isHd(newTx)
|
return pi
|
||||||
|
.isHd(newTx)
|
||||||
.then(isHd => nextHd(t, isHd, newTx))
|
.then(isHd => nextHd(t, isHd, newTx))
|
||||||
.then(newTxHd => {
|
.then(newTxHd => {
|
||||||
return pi.newAddress(newTxHd)
|
return pi.newAddress(newTxHd).then(_.merge(newTxHd))
|
||||||
.then(_.merge(newTxHd))
|
|
||||||
})
|
})
|
||||||
.then(addressedTx => {
|
.then(addressedTx => {
|
||||||
const rec = {
|
const rec = {
|
||||||
to_address: addressedTx.toAddress,
|
to_address: addressedTx.toAddress,
|
||||||
layer_2_address: addressedTx.layer2Address
|
layer_2_address: addressedTx.layer2Address,
|
||||||
}
|
}
|
||||||
|
|
||||||
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
|
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
pi.notifyOperator(newTx, { isRedemption: false, error: 'Error while provisioning address' })
|
pi.notifyOperator(newTx, {
|
||||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
isRedemption: false,
|
||||||
return cashOutActions.logError(t, 'provisionAddress', err, newTx)
|
error: 'Error while provisioning address',
|
||||||
.then(() => { throw err })
|
}).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))
|
return Promise.resolve(updateStatus(oldTx, newTx)).then(updatedTx => {
|
||||||
.then(updatedTx => {
|
if (updatedTx.status !== oldTx.status) {
|
||||||
if (updatedTx.status !== oldTx.status) {
|
const isZeroConf = pi.isZeroConf(updatedTx)
|
||||||
const isZeroConf = pi.isZeroConf(updatedTx)
|
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
|
||||||
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
|
|
||||||
|
|
||||||
const rec = {
|
const rec = {
|
||||||
to_address: updatedTx.toAddress,
|
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
|
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
|
||||||
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
}
|
||||||
|
|
||||||
if (hasError || hasDispenseOccurred) {
|
const hasError = !oldTx.error && newTx.error
|
||||||
return cashOutActions.logDispense(t, updatedTx)
|
const hasDispenseOccurred =
|
||||||
.then(it => updateCassettes(t, updatedTx).then(() => it) )
|
!oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||||
.then((t) => {
|
|
||||||
pi.notifyOperator(updatedTx, { isRedemption: true })
|
|
||||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
|
||||||
return t
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldTx.phone && newTx.phone) {
|
if (hasError || hasDispenseOccurred) {
|
||||||
return cashOutActions.logAction(t, 'addPhone', {}, 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),
|
||||||
|
)
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!oldTx.redeem && newTx.redeem) {
|
if (!oldTx.phone && newTx.phone) {
|
||||||
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
|
return cashOutActions.logAction(t, 'addPhone', {}, updatedTx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedTx
|
if (!oldTx.redeem && newTx.redeem) {
|
||||||
})
|
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTx
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextHd (t, isHd, tx) {
|
function nextHd(t, isHd, tx) {
|
||||||
if (!isHd) return Promise.resolve(tx)
|
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))
|
.then(row => _.set('hdIndex', row.hd_index, tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCassettes (t, tx) {
|
function updateCassettes(t, tx) {
|
||||||
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
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 returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills))
|
||||||
|
|
||||||
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
|
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
|
||||||
|
|
||||||
const values = []
|
const values = []
|
||||||
|
|
||||||
_.forEach(it => values.push(
|
_.forEach(
|
||||||
tx.bills[it].dispensed + tx.bills[it].rejected
|
it => values.push(tx.bills[it].dispensed + tx.bills[it].rejected),
|
||||||
), _.times(_.identity(), _.size(tx.bills)))
|
_.times(_.identity(), _.size(tx.bills)),
|
||||||
|
)
|
||||||
|
|
||||||
values.push(tx.deviceId)
|
values.push(tx.deviceId)
|
||||||
|
|
||||||
return t.one(sql, values)
|
return t.one(sql, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
|
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'])
|
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
|
||||||
|
|
||||||
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized', 'rejected']) &&
|
const isConfirmed = () =>
|
||||||
_.includes(newTx.status, ['instant', 'confirmed'])
|
_.includes(oldTx.status, [
|
||||||
|
'notSeen',
|
||||||
|
'published',
|
||||||
|
'authorized',
|
||||||
|
'rejected',
|
||||||
|
]) && _.includes(newTx.status, ['instant', 'confirmed'])
|
||||||
|
|
||||||
return isZeroConf ? isAuthorized() : isConfirmed()
|
return isZeroConf ? isAuthorized() : isConfirmed()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPublished (status) {
|
function isPublished(status) {
|
||||||
return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed'])
|
return _.includes(status, [
|
||||||
|
'published',
|
||||||
|
'rejected',
|
||||||
|
'authorized',
|
||||||
|
'instant',
|
||||||
|
'confirmed',
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConfirmed (status) {
|
function isConfirmed(status) {
|
||||||
return status === 'confirmed'
|
return status === 'confirmed'
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatus (oldTx, newTx) {
|
function updateStatus(oldTx, newTx) {
|
||||||
const oldStatus = oldTx.status
|
const oldStatus = oldTx.status
|
||||||
const newStatus = ratchetStatus(oldStatus, newTx.status)
|
const newStatus = ratchetStatus(oldStatus, newTx.status)
|
||||||
|
|
||||||
const publishedAt = !oldTx.publishedAt && isPublished(newStatus)
|
const publishedAt =
|
||||||
? 'now()^'
|
!oldTx.publishedAt && isPublished(newStatus) ? 'now()^' : undefined
|
||||||
: undefined
|
|
||||||
|
|
||||||
const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus)
|
const confirmedAt =
|
||||||
? 'now()^'
|
!oldTx.confirmedAt && isConfirmed(newStatus) ? 'now()^' : undefined
|
||||||
: undefined
|
|
||||||
|
|
||||||
const updateRec = {
|
const updateRec = {
|
||||||
publishedAt,
|
publishedAt,
|
||||||
confirmedAt,
|
confirmedAt,
|
||||||
status: newStatus
|
status: newStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.merge(newTx, updateRec)
|
return _.merge(newTx, updateRec)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ratchetStatus (oldStatus, newStatus) {
|
function ratchetStatus(oldStatus, newStatus) {
|
||||||
const statusOrder = ['notSeen', 'published', 'rejected',
|
const statusOrder = [
|
||||||
'authorized', 'instant', 'confirmed']
|
'notSeen',
|
||||||
|
'published',
|
||||||
|
'rejected',
|
||||||
|
'authorized',
|
||||||
|
'instant',
|
||||||
|
'confirmed',
|
||||||
|
]
|
||||||
|
|
||||||
if (oldStatus === newStatus) return oldStatus
|
if (oldStatus === newStatus) return oldStatus
|
||||||
if (newStatus === 'insufficientFunds') return newStatus
|
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]
|
return statusOrder[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispenseOccurred (bills) {
|
function dispenseOccurred(bills) {
|
||||||
if (_.isEmpty(bills)) return false
|
if (_.isEmpty(bills)) return false
|
||||||
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
|
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,18 +40,31 @@ const SNAKE_CASE_BILL_FIELDS = [
|
||||||
'provisioned_recycler_3',
|
'provisioned_recycler_3',
|
||||||
'provisioned_recycler_4',
|
'provisioned_recycler_4',
|
||||||
'provisioned_recycler_5',
|
'provisioned_recycler_5',
|
||||||
'provisioned_recycler_6'
|
'provisioned_recycler_6',
|
||||||
]
|
]
|
||||||
|
|
||||||
const BILL_FIELDS = _.map(_.camelCase, SNAKE_CASE_BILL_FIELDS)
|
const BILL_FIELDS = _.map(_.camelCase, SNAKE_CASE_BILL_FIELDS)
|
||||||
|
|
||||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
module.exports = {
|
||||||
|
redeemableTxs,
|
||||||
|
toObj,
|
||||||
|
toDb,
|
||||||
|
REDEEMABLE_AGE,
|
||||||
|
CASH_OUT_TRANSACTION_STATES,
|
||||||
|
}
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const mapValuesWithKey = _.mapValues.convert({ cap: false })
|
||||||
|
|
||||||
function convertBigNumFields (obj) {
|
function convertBigNumFields(obj) {
|
||||||
const convert = (value, key) => {
|
const convert = (value, key) => {
|
||||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) {
|
if (
|
||||||
|
_.includes(key, [
|
||||||
|
'cryptoAtoms',
|
||||||
|
'receivedCryptoAtoms',
|
||||||
|
'fiat',
|
||||||
|
'fixedFee',
|
||||||
|
])
|
||||||
|
) {
|
||||||
// BACKWARDS_COMPATIBILITY 10.1
|
// BACKWARDS_COMPATIBILITY 10.1
|
||||||
// bills before 10.2 don't have fixedFee
|
// bills before 10.2 don't have fixedFee
|
||||||
if (key === 'fixedFee' && !value) return new BN(0).toString()
|
if (key === 'fixedFee' && !value) return new BN(0).toString()
|
||||||
|
|
@ -59,62 +72,62 @@ function convertBigNumFields (obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only test isNil for these fields since the others should not be empty.
|
// 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.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
const convertKey = key =>
|
||||||
? key + '#'
|
_.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key
|
||||||
: key
|
|
||||||
|
|
||||||
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertField (key) {
|
function convertField(key) {
|
||||||
return _.snakeCase(key)
|
return _.snakeCase(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDbBills (tx) {
|
function addDbBills(tx) {
|
||||||
const bills = tx.bills
|
const bills = tx.bills
|
||||||
if (_.isEmpty(bills)) return tx
|
if (_.isEmpty(bills)) return tx
|
||||||
|
|
||||||
const billsObj = _.flow(
|
const billsObj = _.flow(
|
||||||
_.reduce(
|
_.reduce((acc, value) => {
|
||||||
(acc, value) => {
|
const suffix = _.snakeCase(value.name.replace(/cassette/gi, ''))
|
||||||
const suffix = _.snakeCase(value.name.replace(/cassette/gi, ''))
|
return {
|
||||||
return {
|
...acc,
|
||||||
...acc,
|
[`provisioned_${suffix}`]: value.provisioned,
|
||||||
[`provisioned_${suffix}`]: value.provisioned,
|
[`denomination_${suffix}`]: value.denomination,
|
||||||
[`denomination_${suffix}`]: value.denomination
|
}
|
||||||
}
|
}, {}),
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
it => {
|
it => {
|
||||||
const missingKeys = _.reduce(
|
const missingKeys = _.reduce((acc, value) => {
|
||||||
(acc, value) => {
|
return _.assign({ [value]: 0 })(acc)
|
||||||
return _.assign({ [value]: 0 })(acc)
|
}, {})(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it)))
|
||||||
},
|
|
||||||
{}
|
|
||||||
)(_.difference(SNAKE_CASE_BILL_FIELDS, _.keys(it)))
|
|
||||||
return _.assign(missingKeys, it)
|
return _.assign(missingKeys, it)
|
||||||
}
|
},
|
||||||
)(bills)
|
)(bills)
|
||||||
|
|
||||||
return _.assign(tx, billsObj)
|
return _.assign(tx, billsObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDb (tx) {
|
function toDb(tx) {
|
||||||
const massager = _.flow(convertBigNumFields, addDbBills,
|
const massager = _.flow(
|
||||||
_.omit(['direction', 'bills', 'promoCodeApplied']), _.mapKeys(convertField))
|
convertBigNumFields,
|
||||||
|
addDbBills,
|
||||||
|
_.omit(['direction', 'bills', 'promoCodeApplied']),
|
||||||
|
_.mapKeys(convertField),
|
||||||
|
)
|
||||||
|
|
||||||
return massager(tx)
|
return massager(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toObj (row) {
|
function toObj(row) {
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
|
||||||
const keys = _.keys(row)
|
const keys = _.keys(row)
|
||||||
|
|
@ -126,7 +139,14 @@ function toObj (row) {
|
||||||
newObj[objKey] = new BN(row[key])
|
newObj[objKey] = new BN(row[key])
|
||||||
return
|
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])
|
newObj[objKey] = new BN(row[key])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -137,11 +157,20 @@ function toObj (row) {
|
||||||
newObj.direction = 'cashOut'
|
newObj.direction = 'cashOut'
|
||||||
|
|
||||||
if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj
|
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(
|
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 => ({
|
||||||
_.map(it => ({ name: `recycler${it + 1}`, denomination: newObj[`denominationRecycler${it + 1}`], provisioned: newObj[`provisionedRecycler${it + 1}`] }))(_.range(0, MAX_RECYCLERS)),
|
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.
|
// There can't be bills with denomination === 0.
|
||||||
|
|
@ -151,7 +180,7 @@ function toObj (row) {
|
||||||
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
|
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
function redeemableTxs (deviceId) {
|
function redeemableTxs(deviceId) {
|
||||||
const sql = `select * from cash_out_txs
|
const sql = `select * from cash_out_txs
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
and redeem=$2
|
and redeem=$2
|
||||||
|
|
@ -164,6 +193,5 @@ function redeemableTxs (deviceId) {
|
||||||
)
|
)
|
||||||
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
||||||
|
|
||||||
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE]).then(_.map(toObj))
|
||||||
.then(_.map(toObj))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,73 +7,91 @@ const { anonymousCustomer } = require('../constants')
|
||||||
const toDb = helper.toDb
|
const toDb = helper.toDb
|
||||||
const toObj = helper.toObj
|
const toObj = helper.toObj
|
||||||
|
|
||||||
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
|
const UPDATEABLE_FIELDS = [
|
||||||
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
|
'txHash',
|
||||||
'receivedCryptoAtoms', 'walletScore', 'customerId' ]
|
'txVersion',
|
||||||
|
'status',
|
||||||
|
'dispense',
|
||||||
|
'dispenseConfirmed',
|
||||||
|
'notified',
|
||||||
|
'redeem',
|
||||||
|
'phone',
|
||||||
|
'error',
|
||||||
|
'swept',
|
||||||
|
'publishedAt',
|
||||||
|
'confirmedAt',
|
||||||
|
'errorCode',
|
||||||
|
'receivedCryptoAtoms',
|
||||||
|
'walletScore',
|
||||||
|
'customerId',
|
||||||
|
]
|
||||||
|
|
||||||
module.exports = {upsert, update, insert}
|
module.exports = { upsert, update, insert }
|
||||||
|
|
||||||
function upsert (t, oldTx, tx) {
|
function upsert(t, oldTx, tx) {
|
||||||
if (!oldTx) {
|
if (!oldTx) {
|
||||||
return insert(t, tx)
|
return insert(t, tx).then(newTx => [oldTx, newTx])
|
||||||
.then(newTx => [oldTx, newTx])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return update(t, tx, diff(oldTx, tx))
|
return update(t, tx, diff(oldTx, tx)).then(newTx => [
|
||||||
.then(newTx => [oldTx, newTx, tx.justAuthorized])
|
oldTx,
|
||||||
|
newTx,
|
||||||
|
tx.justAuthorized,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert (t, tx) {
|
function insert(t, tx) {
|
||||||
const dbTx = toDb(tx)
|
const dbTx = toDb(tx)
|
||||||
|
|
||||||
const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *'
|
const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *'
|
||||||
return t.one(sql)
|
return t.one(sql).then(toObj)
|
||||||
.then(toObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (t, tx, changes) {
|
function update(t, tx, changes) {
|
||||||
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
||||||
|
|
||||||
const dbChanges = toDb(changes)
|
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])
|
pgp.as.format(' where id=$1', [tx.id])
|
||||||
|
|
||||||
const newTx = _.merge(tx, changes)
|
const newTx = _.merge(tx, changes)
|
||||||
|
|
||||||
return t.none(sql)
|
return t.none(sql).then(() => newTx)
|
||||||
.then(() => newTx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function diff (oldTx, newTx) {
|
function diff(oldTx, newTx) {
|
||||||
let updatedTx = {}
|
let updatedTx = {}
|
||||||
|
|
||||||
UPDATEABLE_FIELDS.forEach(fieldKey => {
|
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
|
// 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) {
|
switch (fieldKey) {
|
||||||
case 'customerId':
|
case 'customerId':
|
||||||
if (oldTx.customerId === anonymousCustomer.uuid) {
|
if (oldTx.customerId === anonymousCustomer.uuid) {
|
||||||
return updatedTx['customerId'] = newTx['customerId']
|
return (updatedTx['customerId'] = newTx['customerId'])
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
// prevent dispense changing from 'true' to 'false'
|
// prevent dispense changing from 'true' to 'false'
|
||||||
case 'dispense':
|
case 'dispense':
|
||||||
if (!oldTx.dispense) {
|
if (!oldTx.dispense) {
|
||||||
return updatedTx[fieldKey] = newTx[fieldKey]
|
return (updatedTx[fieldKey] = newTx[fieldKey])
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return updatedTx[fieldKey] = newTx[fieldKey]
|
return (updatedTx[fieldKey] = newTx[fieldKey])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return updatedTx
|
return updatedTx
|
||||||
}
|
}
|
||||||
|
|
||||||
function nilEqual (a, b) {
|
function nilEqual(a, b) {
|
||||||
if (_.isNil(a) && _.isNil(b)) return true
|
if (_.isNil(a) && _.isNil(b)) return true
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ module.exports = {
|
||||||
monitorLiveIncoming,
|
monitorLiveIncoming,
|
||||||
monitorStaleIncoming,
|
monitorStaleIncoming,
|
||||||
monitorUnnotified,
|
monitorUnnotified,
|
||||||
cancel
|
cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
const STALE_INCOMING_TX_AGE = T.day
|
const STALE_INCOMING_TX_AGE = T.day
|
||||||
|
|
@ -31,38 +31,40 @@ const INSUFFICIENT_FUNDS_CODE = 570
|
||||||
|
|
||||||
const toObj = helper.toObj
|
const toObj = helper.toObj
|
||||||
|
|
||||||
function selfPost (tx, pi) {
|
function selfPost(tx, pi) {
|
||||||
return post(tx, pi, false)
|
return post(tx, pi, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function post (tx, pi, fromClient = true) {
|
function post(tx, pi, fromClient = true) {
|
||||||
logger.silly('Updating cashout -- tx:', JSON.stringify(tx))
|
logger.silly('Updating cashout -- tx:', JSON.stringify(tx))
|
||||||
logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient))
|
logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient))
|
||||||
return cashOutAtomic.atomic(tx, pi, fromClient)
|
return cashOutAtomic.atomic(tx, pi, fromClient).then(txVector => {
|
||||||
.then(txVector => {
|
const [, newTx, justAuthorized] = txVector
|
||||||
const [, newTx, justAuthorized] = txVector
|
return postProcess(txVector, justAuthorized, pi).then(changes =>
|
||||||
return postProcess(txVector, justAuthorized, pi)
|
cashOutLow.update(db, newTx, changes),
|
||||||
.then(changes => cashOutLow.update(db, newTx, changes))
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function postProcess (txVector, justAuthorized, pi) {
|
function postProcess(txVector, justAuthorized, pi) {
|
||||||
const [oldTx, newTx] = txVector
|
const [oldTx, newTx] = txVector
|
||||||
|
|
||||||
if (justAuthorized) {
|
if (justAuthorized) {
|
||||||
pi.sell(newTx)
|
pi.sell(newTx)
|
||||||
pi.notifyOperator(newTx, { isRedemption: false })
|
pi.notifyOperator(newTx, { isRedemption: false }).catch(err =>
|
||||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
logger.error('Failure sending transaction notification', err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||||
return pi.buildAvailableUnits(newTx.id)
|
return pi
|
||||||
|
.buildAvailableUnits(newTx.id)
|
||||||
.then(units => {
|
.then(units => {
|
||||||
units = _.concat(units.cassettes, units.recyclers)
|
units = _.concat(units.cassettes, units.recyclers)
|
||||||
logger.silly('Computing bills to dispense:', {
|
logger.silly('Computing bills to dispense:', {
|
||||||
txId: newTx.id,
|
txId: newTx.id,
|
||||||
units: units,
|
units: units,
|
||||||
fiat: newTx.fiat
|
fiat: newTx.fiat,
|
||||||
})
|
})
|
||||||
const bills = billMath.makeChange(units, newTx.fiat)
|
const bills = billMath.makeChange(units, newTx.fiat)
|
||||||
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||||
|
|
@ -73,27 +75,38 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
.then(bills => {
|
.then(bills => {
|
||||||
const rec = {}
|
const rec = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(
|
||||||
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
|
it => {
|
||||||
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
|
||||||
rec[`denomination_${suffix}`] = bills[it].denomination
|
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
rec[`denomination_${suffix}`] = bills[it].denomination
|
||||||
|
},
|
||||||
|
_.times(_.identity(), _.size(bills)),
|
||||||
|
)
|
||||||
|
|
||||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
return cashOutActions
|
||||||
|
.logAction(db, 'provisionNotes', rec, newTx)
|
||||||
.then(_.constant({ bills }))
|
.then(_.constant({ bills }))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
pi.notifyOperator(newTx, { error: err.message, isRedemption: true })
|
pi.notifyOperator(newTx, {
|
||||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
error: err.message,
|
||||||
return cashOutActions.logError(db, 'provisionNotesError', err, newTx)
|
isRedemption: true,
|
||||||
.then(() => { throw err })
|
}).catch(err =>
|
||||||
|
logger.error('Failure sending transaction notification', err),
|
||||||
|
)
|
||||||
|
return cashOutActions
|
||||||
|
.logError(db, 'provisionNotesError', err, newTx)
|
||||||
|
.then(() => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchOpenTxs (statuses, fromAge, toAge) {
|
function fetchOpenTxs(statuses, fromAge, toAge) {
|
||||||
const sql = `select *
|
const sql = `select *
|
||||||
from cash_out_txs
|
from cash_out_txs
|
||||||
where ((extract(epoch from (now() - created))) * 1000)>$1
|
where ((extract(epoch from (now() - created))) * 1000)>$1
|
||||||
|
|
@ -103,20 +116,27 @@ function fetchOpenTxs (statuses, fromAge, toAge) {
|
||||||
|
|
||||||
const statusClause = _.map(pgp.as.text, statuses).join(',')
|
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))
|
.then(rows => rows.map(toObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
function processTxStatus (tx, settings) {
|
function processTxStatus(tx, settings) {
|
||||||
const pi = plugins(settings, tx.deviceId)
|
const pi = plugins(settings, tx.deviceId)
|
||||||
|
|
||||||
return pi.getStatus(tx)
|
return pi
|
||||||
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
|
.getStatus(tx)
|
||||||
|
.then(res =>
|
||||||
|
_.assign(tx, {
|
||||||
|
receivedCryptoAtoms: res.receivedCryptoAtoms,
|
||||||
|
status: res.status,
|
||||||
|
}),
|
||||||
|
)
|
||||||
.then(_tx => getWalletScore(_tx, pi))
|
.then(_tx => getWalletScore(_tx, pi))
|
||||||
.then(_tx => selfPost(_tx, pi))
|
.then(_tx => selfPost(_tx, pi))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore(tx, pi) {
|
||||||
const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds']
|
const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds']
|
||||||
|
|
||||||
if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) {
|
if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) {
|
||||||
|
|
@ -124,40 +144,54 @@ function getWalletScore (tx, pi) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transaction shows up on the blockchain, we can request the sender address
|
// Transaction shows up on the blockchain, we can request the sender address
|
||||||
return pi.isWalletScoringEnabled(tx)
|
return pi.isWalletScoringEnabled(tx).then(isEnabled => {
|
||||||
.then(isEnabled => {
|
if (!isEnabled) return tx
|
||||||
if (!isEnabled) return tx
|
return pi
|
||||||
return pi.rateTransaction(tx)
|
.rateTransaction(tx)
|
||||||
.then(res =>
|
.then(res =>
|
||||||
res.isValid
|
res.isValid
|
||||||
? _.assign(tx, { walletScore: res.score })
|
? _.assign(tx, { walletScore: res.score })
|
||||||
: _.assign(tx, {
|
: _.assign(tx, {
|
||||||
walletScore: res.score,
|
walletScore: res.score,
|
||||||
error: 'Chain analysis score is above defined threshold',
|
error: 'Chain analysis score is above defined threshold',
|
||||||
errorCode: 'scoreThresholdReached',
|
errorCode: 'scoreThresholdReached',
|
||||||
dispense: true
|
dispense: true,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.catch(error => _.assign(tx, {
|
.catch(error =>
|
||||||
|
_.assign(tx, {
|
||||||
walletScore: 10,
|
walletScore: 10,
|
||||||
error: `Failure getting address score: ${error.message}`,
|
error: `Failure getting address score: ${error.message}`,
|
||||||
errorCode: 'walletScoringError',
|
errorCode: 'walletScoringError',
|
||||||
dispense: true
|
dispense: true,
|
||||||
}))
|
}),
|
||||||
})
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorLiveIncoming (settings) {
|
function monitorLiveIncoming(settings) {
|
||||||
const statuses = ['notSeen', 'published', 'insufficientFunds']
|
const statuses = ['notSeen', 'published', 'insufficientFunds']
|
||||||
return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
|
return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorStaleIncoming (settings) {
|
function monitorStaleIncoming(settings) {
|
||||||
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
|
const statuses = [
|
||||||
return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
|
'notSeen',
|
||||||
|
'published',
|
||||||
|
'authorized',
|
||||||
|
'instant',
|
||||||
|
'rejected',
|
||||||
|
'insufficientFunds',
|
||||||
|
]
|
||||||
|
return monitorIncoming(
|
||||||
|
settings,
|
||||||
|
statuses,
|
||||||
|
STALE_LIVE_INCOMING_TX_AGE,
|
||||||
|
STALE_INCOMING_TX_AGE,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorIncoming (settings, statuses, fromAge, toAge) {
|
function monitorIncoming(settings, statuses, fromAge, toAge) {
|
||||||
return fetchOpenTxs(statuses, fromAge, toAge)
|
return fetchOpenTxs(statuses, fromAge, toAge)
|
||||||
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
|
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
@ -169,7 +203,7 @@ function monitorIncoming (settings, statuses, fromAge, toAge) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorUnnotified (settings) {
|
function monitorUnnotified(settings) {
|
||||||
const sql = `select *
|
const sql = `select *
|
||||||
from cash_out_txs
|
from cash_out_txs
|
||||||
where ((extract(epoch from (now() - created))) * 1000)<$1
|
where ((extract(epoch from (now() - created))) * 1000)<$1
|
||||||
|
|
@ -179,23 +213,26 @@ function monitorUnnotified (settings) {
|
||||||
and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)`
|
and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)`
|
||||||
|
|
||||||
const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx)
|
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(rows => _.map(toObj, rows))
|
||||||
.then(txs => Promise.all(txs.map(notify)))
|
.then(txs => Promise.all(txs.map(notify)))
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel (txId) {
|
function cancel(txId) {
|
||||||
const updateRec = {
|
const updateRec = {
|
||||||
error: 'Operator cancel',
|
error: 'Operator cancel',
|
||||||
error_code: 'operatorCancel',
|
error_code: 'operatorCancel',
|
||||||
dispense: true
|
dispense: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return pgp.helpers.update(updateRec, null, 'cash_out_txs') +
|
return (
|
||||||
pgp.as.format(' where id=$1', [txId])
|
pgp.helpers.update(updateRec, null, 'cash_out_txs') +
|
||||||
|
pgp.as.format(' where id=$1', [txId])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.then(sql => db.result(sql, false))
|
.then(sql => db.result(sql, false))
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ const _ = require('lodash/fp')
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
const camelize = require('./utils')
|
const camelize = require('./utils')
|
||||||
|
|
||||||
function createCashboxBatch (deviceId, cashboxCount) {
|
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 sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
|
||||||
const sql2 = `
|
const sql2 = `
|
||||||
UPDATE bills SET cashbox_batch_id=$1
|
UPDATE bills SET cashbox_batch_id=$1
|
||||||
|
|
@ -24,55 +25,93 @@ function createCashboxBatch (deviceId, cashboxCount) {
|
||||||
const q1 = t.one(sql, [batchId, deviceId])
|
const q1 = t.one(sql, [batchId, deviceId])
|
||||||
const q2 = t.none(sql2, [batchId, deviceId])
|
const q2 = t.none(sql2, [batchId, deviceId])
|
||||||
const q3 = t.none(sql3, [batchId, deviceId])
|
const q3 = t.none(sql3, [batchId, deviceId])
|
||||||
return t.batch([q1, q2, q3])
|
return t.batch([q1, q2, q3]).then(([it]) => it)
|
||||||
.then(([it]) => it)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMachineWithBatch (machineContext, oldCashboxCount) {
|
function updateMachineWithBatch(machineContext, oldCashboxCount) {
|
||||||
const cashUnits = machineContext.cashUnits
|
const cashUnits = machineContext.cashUnits
|
||||||
const cashUnitNames = ['cashbox', 'cassette1', 'cassette2', 'cassette3', 'cassette4', 'recycler1', 'recycler2', 'recycler3', 'recycler4', 'recycler5', 'recycler6']
|
const cashUnitNames = [
|
||||||
const isValidContext = _.has(['deviceId', 'cashUnits'], machineContext) && _.has(cashUnitNames, cashUnits)
|
'cashbox',
|
||||||
const cassettes = _.filter(it => !_.isNil(it))([cashUnits.cassette1, cashUnits.cassette2, cashUnits.cassette3, cashUnits.cassette4])
|
'cassette1',
|
||||||
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(cassettes))
|
'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)
|
if (!isValidContext && !isCassetteAmountWithinRange)
|
||||||
throw new Error('Insufficient info to create a new cashbox batch')
|
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 => {
|
return db.tx(t => {
|
||||||
const deviceId = machineContext.deviceId
|
const deviceId = machineContext.deviceId
|
||||||
const batchId = uuid.v4()
|
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 q1 = t.none(
|
||||||
const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
|
`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
|
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||||
cash_in_txs.device_id = $2 AND
|
cash_in_txs.device_id = $2 AND
|
||||||
bills.destination_unit = 'cashbox' AND
|
bills.destination_unit = 'cashbox' AND
|
||||||
bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
bills.cashbox_batch_id IS NULL`,
|
||||||
const q3 = t.none(`UPDATE empty_unit_bills SET cashbox_batch_id=$1
|
[batchId, deviceId],
|
||||||
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
)
|
||||||
const q4 = t.none(`
|
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,
|
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
|
||||||
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
|
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
|
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6) WHERE device_id=$11
|
||||||
`, [
|
`,
|
||||||
cashUnits.cassette1,
|
[
|
||||||
cashUnits.cassette2,
|
cashUnits.cassette1,
|
||||||
cashUnits.cassette3,
|
cashUnits.cassette2,
|
||||||
cashUnits.cassette4,
|
cashUnits.cassette3,
|
||||||
cashUnits.recycler1,
|
cashUnits.cassette4,
|
||||||
cashUnits.recycler2,
|
cashUnits.recycler1,
|
||||||
cashUnits.recycler3,
|
cashUnits.recycler2,
|
||||||
cashUnits.recycler4,
|
cashUnits.recycler3,
|
||||||
cashUnits.recycler5,
|
cashUnits.recycler4,
|
||||||
cashUnits.recycler6,
|
cashUnits.recycler5,
|
||||||
machineContext.deviceId
|
cashUnits.recycler6,
|
||||||
])
|
machineContext.deviceId,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
return t.batch([q1, q2, q3, q4])
|
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 = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
cuo.id,
|
cuo.id,
|
||||||
|
|
@ -96,28 +135,25 @@ function getBatches (from = new Date(0).toISOString(), until = new Date().toISOS
|
||||||
return db.any(sql, [from, until]).then(camelize)
|
return db.any(sql, [from, until]).then(camelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
function editBatchById (id, performedBy) {
|
function editBatchById(id, performedBy) {
|
||||||
const sql = `UPDATE cash_unit_operation SET performed_by=$1 WHERE id=$2 AND cuo.operation_type = 'cash-box-empty'`
|
const sql = `UPDATE cash_unit_operation SET performed_by=$1 WHERE id=$2 AND cuo.operation_type = 'cash-box-empty'`
|
||||||
return db.none(sql, [performedBy, id])
|
return db.none(sql, [performedBy, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
function logFormatter (data) {
|
function logFormatter(data) {
|
||||||
return _.map(
|
return _.map(it => {
|
||||||
it => {
|
return {
|
||||||
return {
|
id: it.id,
|
||||||
id: it.id,
|
deviceId: it.deviceId,
|
||||||
deviceId: it.deviceId,
|
created: it.created,
|
||||||
created: it.created,
|
operationType: it.operationType,
|
||||||
operationType: it.operationType,
|
billCount: it.billCount,
|
||||||
billCount: it.billCount,
|
fiatTotal: it.fiatTotal,
|
||||||
fiatTotal: it.fiatTotal
|
}
|
||||||
}
|
}, data)
|
||||||
},
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineUnbatchedBills (deviceId) {
|
function getMachineUnbatchedBills(deviceId) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs
|
SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs
|
||||||
ON b.cash_in_txs_id = cash_in_txs.id
|
ON b.cash_in_txs_id = cash_in_txs.id
|
||||||
|
|
@ -125,12 +161,13 @@ function getMachineUnbatchedBills (deviceId) {
|
||||||
GROUP BY cash_in_txs.device_id
|
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(res => _.mapKeys(it => _.camelCase(it), res))
|
||||||
.then(logFormatterSingle)
|
.then(logFormatterSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBatchById (id) {
|
function getBatchById(id) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
||||||
FROM cash_unit_operation AS cb
|
FROM cash_unit_operation AS cb
|
||||||
|
|
@ -139,14 +176,22 @@ function getBatchById (id) {
|
||||||
GROUP BY cb.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)
|
.then(logFormatterSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logFormatterSingle (data) {
|
function logFormatterSingle(data) {
|
||||||
const bills = _.filter(
|
const bills = _.filter(
|
||||||
it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)),
|
it =>
|
||||||
data.bills
|
!(
|
||||||
|
_.isNil(it) ||
|
||||||
|
_.isNil(it.fiat_code) ||
|
||||||
|
_.isNil(it.fiat) ||
|
||||||
|
_.isNaN(it.fiat)
|
||||||
|
),
|
||||||
|
data.bills,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -161,9 +206,9 @@ function logFormatterSingle (data) {
|
||||||
return acc
|
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,
|
editBatchById,
|
||||||
getBatchById,
|
getBatchById,
|
||||||
getMachineUnbatchedBills,
|
getMachineUnbatchedBills,
|
||||||
logFormatter
|
logFormatter,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,13 @@
|
||||||
*/
|
*/
|
||||||
const prepare_denominations = denominations =>
|
const prepare_denominations = denominations =>
|
||||||
JSON.parse(JSON.stringify(denominations))
|
JSON.parse(JSON.stringify(denominations))
|
||||||
.sort(([d1, c1], [d2, c2]) => d1 < d2)
|
.sort(([d1], [d2]) => d1 < d2)
|
||||||
.reduce(
|
.reduce(
|
||||||
([csum, denoms], [denom, count]) => {
|
([csum, denoms], [denom, count]) => {
|
||||||
csum += denom*count
|
csum += denom * count
|
||||||
return [
|
return [csum, [{ denom, count, csum }].concat(denoms)]
|
||||||
csum,
|
|
||||||
[{ denom, count, csum }].concat(denoms)
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
[0, []]
|
[0, []],
|
||||||
)[1] /* ([csum, denoms]) => denoms */
|
)[1] /* ([csum, denoms]) => denoms */
|
||||||
|
|
||||||
const max_denomination_multiplicity = (denom, count, target) =>
|
const max_denomination_multiplicity = (denom, count, target) =>
|
||||||
|
|
@ -38,18 +35,18 @@ const memo_get = (memo, target, denom) => {
|
||||||
|
|
||||||
const memo_set = (memo, target, denom, solution) => {
|
const memo_set = (memo, target, denom, solution) => {
|
||||||
let denom_solutions = memo[target]
|
let denom_solutions = memo[target]
|
||||||
if (denom_solutions === undefined)
|
if (denom_solutions === undefined) memo[target] = denom_solutions = {}
|
||||||
memo[target] = denom_solutions = {}
|
return (denom_solutions[denom] = solution)
|
||||||
return denom_solutions[denom] = solution
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const check = (solution, target) =>
|
const check = (solution, target) =>
|
||||||
!solution
|
!solution ||
|
||||||
|| target === solution.reduce((sum, [denom, provisioned]) => sum + denom*provisioned, 0)
|
target ===
|
||||||
|
solution.reduce((sum, [denom, provisioned]) => sum + denom * provisioned, 0)
|
||||||
|
|
||||||
const model = denominations => ({
|
const model = denominations => ({
|
||||||
denominations: prepare_denominations(denominations),
|
denominations: prepare_denominations(denominations),
|
||||||
memo: {}
|
memo: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -73,16 +70,19 @@ const solve = (model, target) => {
|
||||||
* of the denominations, or if the target is not divisible by any of the
|
* of the denominations, or if the target is not divisible by any of the
|
||||||
* denominations
|
* denominations
|
||||||
*/
|
*/
|
||||||
if (target > csum)
|
if (target > csum) return memo_set(memo, target, denom, false)
|
||||||
return memo_set(memo, target, denom, false)
|
|
||||||
|
|
||||||
let solution = memo_get(memo, target, denom)
|
let solution = memo_get(memo, target, denom)
|
||||||
if (solution === false) continue /* not here, keep looking */
|
if (solution === false) continue /* not here, keep looking */
|
||||||
if (solution) return solution /* we've previously computed a solution */
|
if (solution) return solution /* we've previously computed a solution */
|
||||||
|
|
||||||
/* solution === null */
|
/* solution === null */
|
||||||
for (let nd = max_denomination_multiplicity(denom, count, target); nd >= 0; nd--) {
|
for (
|
||||||
solution = coin_change(didx+1, target - denom*nd)
|
let nd = max_denomination_multiplicity(denom, count, target);
|
||||||
|
nd >= 0;
|
||||||
|
nd--
|
||||||
|
) {
|
||||||
|
solution = coin_change(didx + 1, target - denom * nd)
|
||||||
if (solution)
|
if (solution)
|
||||||
return memo_set(memo, target, denom, [[denom, nd]].concat(solution))
|
return memo_set(memo, target, denom, [[denom, nd]].concat(solution))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ const STALE_INTERVAL = '2 minutes'
|
||||||
|
|
||||||
module.exports = { update }
|
module.exports = { update }
|
||||||
|
|
||||||
function mapCoin (rates, deviceId, settings, cryptoCode) {
|
function mapCoin(rates, deviceId, settings, cryptoCode) {
|
||||||
const config = settings.config
|
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 commissions = configManager.getCommissions(cryptoCode, deviceId, config)
|
||||||
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
||||||
|
|
||||||
|
|
@ -30,8 +32,12 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||||
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
||||||
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
||||||
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
|
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
|
||||||
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
|
const cashInRate = showCommissions
|
||||||
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
|
? _.invoke('cashIn.toNumber', buildedRates)
|
||||||
|
: null
|
||||||
|
const cashOutRate = showCommissions
|
||||||
|
? _.invoke('cashOut.toNumber', buildedRates)
|
||||||
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cryptoCode,
|
cryptoCode,
|
||||||
|
|
@ -40,11 +46,11 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||||
cashInFixedFee,
|
cashInFixedFee,
|
||||||
cashOutFixedFee,
|
cashOutFixedFee,
|
||||||
cashInRate,
|
cashInRate,
|
||||||
cashOutRate
|
cashOutRate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapIdentification (config) {
|
function mapIdentification(config) {
|
||||||
const triggers = configManager.getTriggers(config)
|
const triggers = configManager.getTriggers(config)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -52,11 +58,11 @@ function mapIdentification (config) {
|
||||||
isPalmVein: false,
|
isPalmVein: false,
|
||||||
isPhoto: complianceTriggers.hasFacephoto(triggers),
|
isPhoto: complianceTriggers.hasFacephoto(triggers),
|
||||||
isIdDocScan: complianceTriggers.hasIdScan(triggers),
|
isIdDocScan: complianceTriggers.hasIdScan(triggers),
|
||||||
isFingerprint: false
|
isFingerprint: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapMachine (rates, settings, machineRow) {
|
function mapMachine(rates, settings, machineRow) {
|
||||||
const deviceId = machineRow.device_id
|
const deviceId = machineRow.device_id
|
||||||
const config = settings.config
|
const config = settings.config
|
||||||
|
|
||||||
|
|
@ -69,10 +75,15 @@ function mapMachine (rates, settings, machineRow) {
|
||||||
const lastOnline = machineRow.last_online.toISOString()
|
const lastOnline = machineRow.last_online.toISOString()
|
||||||
const status = machineRow.stale ? 'online' : 'offline'
|
const status = machineRow.stale ? 'online' : 'offline'
|
||||||
const showLimitsAndVerification = coinAtmRadar.limitsAndVerification
|
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 cryptoCurrencies = locale.cryptoCurrencies
|
||||||
const identification = mapIdentification(config)
|
const identification = mapIdentification(config)
|
||||||
const coins = _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
|
const coins = _.map(
|
||||||
|
_.partial(mapCoin, [rates, deviceId, settings]),
|
||||||
|
cryptoCurrencies,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
machineId: deviceId,
|
machineId: deviceId,
|
||||||
address: {
|
address: {
|
||||||
|
|
@ -80,12 +91,12 @@ function mapMachine (rates, settings, machineRow) {
|
||||||
city: null,
|
city: null,
|
||||||
region: null,
|
region: null,
|
||||||
postalCode: null,
|
postalCode: null,
|
||||||
country: null
|
country: null,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
name: null,
|
name: null,
|
||||||
url: null,
|
url: null,
|
||||||
phone: null
|
phone: null,
|
||||||
},
|
},
|
||||||
status,
|
status,
|
||||||
lastOnline,
|
lastOnline,
|
||||||
|
|
@ -98,20 +109,21 @@ function mapMachine (rates, settings, machineRow) {
|
||||||
cashOutDailyLimit: cashLimit,
|
cashOutDailyLimit: cashLimit,
|
||||||
fiatCurrency: locale.fiatCurrency,
|
fiatCurrency: locale.fiatCurrency,
|
||||||
identification,
|
identification,
|
||||||
coins
|
coins,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachines (rates, settings) {
|
function getMachines(rates, settings) {
|
||||||
const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices
|
const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices
|
||||||
where display=TRUE and
|
where display=TRUE and
|
||||||
paired=TRUE
|
paired=TRUE
|
||||||
order by created`
|
order by created`
|
||||||
return db.any(sql, [STALE_INTERVAL])
|
return db
|
||||||
|
.any(sql, [STALE_INTERVAL])
|
||||||
.then(_.map(_.partial(mapMachine, [rates, settings])))
|
.then(_.map(_.partial(mapMachine, [rates, settings])))
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRadar (data) {
|
function sendRadar(data) {
|
||||||
const url = COIN_ATM_RADAR_URL
|
const url = COIN_ATM_RADAR_URL
|
||||||
|
|
||||||
if (_.isEmpty(url)) {
|
if (_.isEmpty(url)) {
|
||||||
|
|
@ -123,31 +135,32 @@ function sendRadar (data) {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
timeout: TIMEOUT,
|
timeout: TIMEOUT,
|
||||||
maxContentLength: MAX_CONTENT_LENGTH
|
maxContentLength: MAX_CONTENT_LENGTH,
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.default(config)
|
return axios.default(config).then(r => logger.info(r.status))
|
||||||
.then(r => logger.info(r.status))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRecord (rates, settings) {
|
function mapRecord(rates, settings) {
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
return Promise.all([getMachines(rates, settings), getOperatorId('coinatmradar')])
|
return Promise.all([
|
||||||
.then(([machines, operatorId]) => {
|
getMachines(rates, settings),
|
||||||
return {
|
getOperatorId('coinatmradar'),
|
||||||
operatorId: operatorId,
|
]).then(([machines, operatorId]) => {
|
||||||
operator: {
|
return {
|
||||||
name: null,
|
operatorId: operatorId,
|
||||||
phone: null,
|
operator: {
|
||||||
email: null
|
name: null,
|
||||||
},
|
phone: null,
|
||||||
timestamp,
|
email: null,
|
||||||
machines
|
},
|
||||||
}
|
timestamp,
|
||||||
})
|
machines,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (rates, settings) {
|
function update(rates, settings) {
|
||||||
const coinAtmRadar = configManager.getCoinAtmRadar(settings.config)
|
const coinAtmRadar = configManager.getCoinAtmRadar(settings.config)
|
||||||
|
|
||||||
if (!coinAtmRadar.active) return Promise.resolve()
|
if (!coinAtmRadar.active) return Promise.resolve()
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ const settings = {
|
||||||
threshold: 123,
|
threshold: 123,
|
||||||
id: '9c3b5af8-b1d1-4125-b169-0e913b33894c',
|
id: '9c3b5af8-b1d1-4125-b169-0e913b33894c',
|
||||||
direction: 'both',
|
direction: 'both',
|
||||||
triggerType: 'txAmount'
|
triggerType: 'txAmount',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requirement: 'sms',
|
requirement: 'sms',
|
||||||
|
|
@ -88,7 +88,7 @@ const settings = {
|
||||||
thresholdDays: 1,
|
thresholdDays: 1,
|
||||||
id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b',
|
id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b',
|
||||||
direction: 'both',
|
direction: 'both',
|
||||||
triggerType: 'txVelocity'
|
triggerType: 'txVelocity',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requirement: 'sms',
|
requirement: 'sms',
|
||||||
|
|
@ -96,58 +96,58 @@ const settings = {
|
||||||
thresholdDays: 1,
|
thresholdDays: 1,
|
||||||
id: '6ac38fe6-172c-48a4-8a7f-605213cbd600',
|
id: '6ac38fe6-172c-48a4-8a7f-605213cbd600',
|
||||||
direction: 'both',
|
direction: 'both',
|
||||||
triggerType: 'txVolume'
|
triggerType: 'txVolume',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
notifications_sms_transactions: true,
|
notifications_sms_transactions: true,
|
||||||
notifications_highValueTransaction: 50
|
notifications_highValueTransaction: 50,
|
||||||
},
|
},
|
||||||
accounts: {}
|
accounts: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const rates = [
|
const rates = [
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(19164.3),
|
ask: new BN(19164.3),
|
||||||
bid: new BN(19164.2)
|
bid: new BN(19164.2),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(594.54),
|
ask: new BN(594.54),
|
||||||
bid: new BN(594.09)
|
bid: new BN(594.09),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(84.38),
|
ask: new BN(84.38),
|
||||||
bid: new BN(84.37)
|
bid: new BN(84.37),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(102.8),
|
ask: new BN(102.8),
|
||||||
bid: new BN(101.64)
|
bid: new BN(101.64),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(74.91),
|
ask: new BN(74.91),
|
||||||
bid: new BN(74.12)
|
bid: new BN(74.12),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rates: {
|
rates: {
|
||||||
ask: new BN(284.4),
|
ask: new BN(284.4),
|
||||||
bid: new BN(284.4)
|
bid: new BN(284.4),
|
||||||
},
|
},
|
||||||
timestamp: +new Date()
|
timestamp: +new Date(),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const dbResponse = [
|
const dbResponse = [
|
||||||
|
|
@ -155,32 +155,32 @@ const dbResponse = [
|
||||||
device_id:
|
device_id:
|
||||||
'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||||
last_online: new Date('2020-11-16T13:11:03.169Z'),
|
last_online: new Date('2020-11-16T13:11:03.169Z'),
|
||||||
stale: false
|
stale: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
device_id:
|
device_id:
|
||||||
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
|
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
|
||||||
last_online: new Date('2020-11-16T16:21:35.948Z'),
|
last_online: new Date('2020-11-16T16:21:35.948Z'),
|
||||||
stale: false
|
stale: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
device_id:
|
device_id:
|
||||||
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
|
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
|
||||||
last_online: new Date('2020-11-19T15:07:57.089Z'),
|
last_online: new Date('2020-11-19T15:07:57.089Z'),
|
||||||
stale: false
|
stale: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
device_id:
|
device_id:
|
||||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||||
last_online: new Date('2020-11-26T20:05:57.792Z'),
|
last_online: new Date('2020-11-26T20:05:57.792Z'),
|
||||||
stale: false
|
stale: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
device_id:
|
device_id:
|
||||||
'490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88',
|
'490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88',
|
||||||
last_online: new Date('2020-12-04T16:48:05.129Z'),
|
last_online: new Date('2020-12-04T16:48:05.129Z'),
|
||||||
stale: false
|
stale: false,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function validateData(data) {
|
function validateData(data) {
|
||||||
|
|
@ -189,7 +189,7 @@ function validateData(data) {
|
||||||
operator: yup.object().shape({
|
operator: yup.object().shape({
|
||||||
name: yup.string().nullable(),
|
name: yup.string().nullable(),
|
||||||
phone: yup.string().nullable(),
|
phone: yup.string().nullable(),
|
||||||
email: yup.string().email().nullable()
|
email: yup.string().email().nullable(),
|
||||||
}),
|
}),
|
||||||
timestamp: yup.string().required('timestamp not provided'),
|
timestamp: yup.string().required('timestamp not provided'),
|
||||||
machines: yup.array().of(
|
machines: yup.array().of(
|
||||||
|
|
@ -200,12 +200,12 @@ function validateData(data) {
|
||||||
city: yup.string().nullable(),
|
city: yup.string().nullable(),
|
||||||
region: yup.string().nullable(),
|
region: yup.string().nullable(),
|
||||||
postalCode: yup.string().nullable(),
|
postalCode: yup.string().nullable(),
|
||||||
country: yup.string().nullable()
|
country: yup.string().nullable(),
|
||||||
}),
|
}),
|
||||||
location: yup.object().required('location object not provided').shape({
|
location: yup.object().required('location object not provided').shape({
|
||||||
name: yup.string().nullable(),
|
name: yup.string().nullable(),
|
||||||
url: yup.string().nullable(),
|
url: yup.string().nullable(),
|
||||||
phone: yup.string().nullable()
|
phone: yup.string().nullable(),
|
||||||
}),
|
}),
|
||||||
status: yup
|
status: yup
|
||||||
.string()
|
.string()
|
||||||
|
|
@ -231,7 +231,7 @@ function validateData(data) {
|
||||||
.required('isIdDocScan boolean not defined'),
|
.required('isIdDocScan boolean not defined'),
|
||||||
isFingerprint: yup
|
isFingerprint: yup
|
||||||
.boolean()
|
.boolean()
|
||||||
.required('isFingerprint boolean not defined')
|
.required('isFingerprint boolean not defined'),
|
||||||
}),
|
}),
|
||||||
coins: yup.array().of(
|
coins: yup.array().of(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
|
|
@ -240,11 +240,11 @@ function validateData(data) {
|
||||||
cashOutFee: yup.number().nullable(),
|
cashOutFee: yup.number().nullable(),
|
||||||
cashInFixedFee: yup.number().nullable(),
|
cashInFixedFee: yup.number().nullable(),
|
||||||
cashInRate: yup.number().nullable(),
|
cashInRate: yup.number().nullable(),
|
||||||
cashOutRate: yup.number().nullable()
|
cashOutRate: yup.number().nullable(),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
})
|
})
|
||||||
return schema.validate(data)
|
return schema.validate(data)
|
||||||
}
|
}
|
||||||
|
|
@ -252,13 +252,13 @@ function validateData(data) {
|
||||||
test('Verify axios request schema', async () => {
|
test('Verify axios request schema', async () => {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
jest.spyOn(axios, 'default').mockImplementation(
|
jest
|
||||||
jest.fn(req =>
|
.spyOn(axios, 'default')
|
||||||
validateData(req.data)
|
.mockImplementation(
|
||||||
.then(() => ({ status: 'mock status 200' }))
|
jest.fn(req =>
|
||||||
.catch(e => fail(e))
|
validateData(req.data).then(() => ({ status: 'mock status 200' })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
db.any.mockResolvedValue(dbResponse)
|
db.any.mockResolvedValue(dbResponse)
|
||||||
await car.update(rates, settings)
|
await car.update(rates, settings)
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,41 @@ const BN = require('./bn')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const { utils: coinUtils } = require('@lamassu/coins')
|
const { utils: coinUtils } = require('@lamassu/coins')
|
||||||
|
|
||||||
function truncateCrypto (cryptoAtoms, cryptoCode) {
|
function truncateCrypto(cryptoAtoms, cryptoCode) {
|
||||||
const DECIMAL_PLACES = 6
|
const DECIMAL_PLACES = 6
|
||||||
if (cryptoAtoms.eq(0)) return cryptoAtoms
|
if (cryptoAtoms.eq(0)) return cryptoAtoms
|
||||||
|
|
||||||
const scale = coinUtils.getCryptoCurrency(cryptoCode).unitScale
|
const scale = coinUtils.getCryptoCurrency(cryptoCode).unitScale
|
||||||
const scaleFactor = BN(10).pow(scale)
|
const scaleFactor = BN(10).pow(scale)
|
||||||
|
|
||||||
return new BN(cryptoAtoms).integerValue(BN.ROUND_DOWN).div(scaleFactor)
|
return new BN(cryptoAtoms)
|
||||||
.decimalPlaces(DECIMAL_PLACES).times(scaleFactor)
|
.integerValue(BN.ROUND_DOWN)
|
||||||
|
.div(scaleFactor)
|
||||||
|
.decimalPlaces(DECIMAL_PLACES)
|
||||||
|
.times(scaleFactor)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fiatToCrypto (tx, rec, deviceId, config) {
|
function fiatToCrypto(tx, rec, deviceId, config) {
|
||||||
const usableFiat = rec.fiat - rec.cashInFee
|
const 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 tickerRate = new BN(tx.rawTickerPrice)
|
||||||
const discount = getDiscountRate(tx.discount, commissions[tx.direction])
|
const discount = getDiscountRate(tx.discount, commissions[tx.direction])
|
||||||
const rate = tickerRate.times(discount).decimalPlaces(5)
|
const rate = tickerRate.times(discount).decimalPlaces(5)
|
||||||
const unitScale = coinUtils.getCryptoCurrency(tx.cryptoCode).unitScale
|
const unitScale = coinUtils.getCryptoCurrency(tx.cryptoCode).unitScale
|
||||||
const unitScaleFactor = new BN(10).pow(unitScale)
|
const unitScaleFactor = new BN(10).pow(unitScale)
|
||||||
|
|
||||||
return truncateCrypto(new BN(usableFiat).div(rate.div(unitScaleFactor)), tx.cryptoCode)
|
return truncateCrypto(
|
||||||
|
new BN(usableFiat).div(rate.div(unitScaleFactor)),
|
||||||
|
tx.cryptoCode,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDiscountRate (discount, commission) {
|
function getDiscountRate(discount, commission) {
|
||||||
const bnDiscount = discount ? new BN(discount) : new BN(0)
|
const bnDiscount = discount ? new BN(discount) : new BN(0)
|
||||||
const bnCommission = new BN(commission)
|
const bnCommission = new BN(commission)
|
||||||
const percentageDiscount = new BN(1).minus(bnDiscount.div(100))
|
const percentageDiscount = new BN(1).minus(bnDiscount.div(100))
|
||||||
|
|
@ -36,5 +46,5 @@ function getDiscountRate (discount, commission) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fiatToCrypto,
|
fiatToCrypto,
|
||||||
getDiscountRate
|
getDiscountRate,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,25 @@ const getPlugin = (settings, pluginCode) => {
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
const plugin = ph.load(ph.COMPLIANCE, pluginCode)
|
const plugin = ph.load(ph.COMPLIANCE, pluginCode)
|
||||||
|
|
||||||
return ({ plugin, account })
|
return { plugin, account }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatus = (settings, service, customerId) => {
|
const getStatus = (settings, service, customerId) => {
|
||||||
try {
|
try {
|
||||||
const { plugin, account } = getPlugin(settings, service)
|
const { plugin, account } = getPlugin(settings, service)
|
||||||
|
|
||||||
return plugin.getApplicantStatus(account, customerId)
|
return plugin
|
||||||
.then((status) => ({
|
.getApplicantStatus(account, customerId)
|
||||||
|
.then(status => ({
|
||||||
service,
|
service,
|
||||||
status
|
status,
|
||||||
}))
|
}))
|
||||||
.catch((error) => {
|
.catch(error => {
|
||||||
if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message)
|
if (error.response.status !== 404)
|
||||||
|
logger.error(
|
||||||
|
`Error getting applicant for service ${service}:`,
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
service: service,
|
service: service,
|
||||||
status: null,
|
status: null,
|
||||||
|
|
@ -34,28 +39,22 @@ const getStatus = (settings, service, customerId) => {
|
||||||
status: null,
|
status: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusMap = (settings, customerExternalCompliance) => {
|
const getStatusMap = (settings, customerExternalCompliance) => {
|
||||||
const triggers = configManager.getTriggers(settings.config)
|
const triggers = configManager.getTriggers(settings.config)
|
||||||
const services = _.flow(
|
const services = _.flow(_.map('externalService'), _.compact, _.uniq)(triggers)
|
||||||
_.map('externalService'),
|
|
||||||
_.compact,
|
|
||||||
_.uniq
|
|
||||||
)(triggers)
|
|
||||||
|
|
||||||
const applicantPromises = _.map(service => {
|
const applicantPromises = _.map(service => {
|
||||||
return getStatus(settings, service, customerExternalCompliance)
|
return getStatus(settings, service, customerExternalCompliance)
|
||||||
})(services)
|
})(services)
|
||||||
|
|
||||||
return Promise.all(applicantPromises)
|
return Promise.all(applicantPromises).then(applicantResults => {
|
||||||
.then((applicantResults) => {
|
return _.reduce((map, result) => {
|
||||||
return _.reduce((map, result) => {
|
if (result.status) map[result.service] = result.status
|
||||||
if (result.status) map[result.service] = result.status
|
return map
|
||||||
return map
|
}, {})(applicantResults)
|
||||||
}, {})(applicantResults)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createApplicant = (settings, externalService, customerId) => {
|
const createApplicant = (settings, externalService, customerId) => {
|
||||||
|
|
@ -76,5 +75,5 @@ module.exports = {
|
||||||
getStatusMap,
|
getStatusMap,
|
||||||
getStatus,
|
getStatus,
|
||||||
createApplicant,
|
createApplicant,
|
||||||
createLink
|
createLink,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,35 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
function getBackwardsCompatibleTriggers (triggers) {
|
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)
|
const grouped = _.groupBy(_.prop('requirement'))(filtered)
|
||||||
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped)
|
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(
|
||||||
|
grouped,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSanctions (triggers) {
|
function hasSanctions(triggers) {
|
||||||
return _.some(_.matches({ requirement: 'sanctions' }))(triggers)
|
return _.some(_.matches({ requirement: 'sanctions' }))(triggers)
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxDaysThreshold (triggers) {
|
function maxDaysThreshold(triggers) {
|
||||||
return _.max(_.map('thresholdDays')(triggers))
|
return _.max(_.map('thresholdDays')(triggers))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCashLimit (triggers) {
|
function getCashLimit(triggers) {
|
||||||
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
|
const withFiat = _.filter(({ triggerType }) =>
|
||||||
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
|
_.includes(triggerType, ['txVolume', 'txAmount']),
|
||||||
|
)
|
||||||
|
const blocking = _.filter(({ requirement }) =>
|
||||||
|
_.includes(requirement, ['block', 'suspend']),
|
||||||
|
)
|
||||||
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
|
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 hasPhone = hasRequirement('sms')
|
||||||
const hasFacephoto = hasRequirement('facephoto')
|
const hasFacephoto = hasRequirement('facephoto')
|
||||||
|
|
@ -28,7 +37,16 @@ const hasIdScan = hasRequirement('idCardData')
|
||||||
|
|
||||||
const AUTH_METHODS = {
|
const AUTH_METHODS = {
|
||||||
SMS: 'SMS',
|
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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,75 +5,87 @@ const logger = require('./logger')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const ofac = require('./ofac/index')
|
const ofac = require('./ofac/index')
|
||||||
|
|
||||||
function logSanctionsMatch (deviceId, customer, sanctionsId, alias) {
|
function logSanctionsMatch(deviceId, customer, sanctionsId, alias) {
|
||||||
const sql = `insert into sanctions_logs
|
const sql = `insert into sanctions_logs
|
||||||
(id, device_id, sanctioned_id, sanctioned_alias_id, sanctioned_alias_full_name, customer_id)
|
(id, device_id, sanctioned_id, sanctioned_alias_id, sanctioned_alias_full_name, customer_id)
|
||||||
values
|
values
|
||||||
($1, $2, $3, $4, $5, $6)`
|
($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) {
|
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)
|
const logResult = result => _.map(logAlias(result.id), result.aliases)
|
||||||
|
|
||||||
return Promise.all(_.flatMap(logResult, results))
|
return Promise.all(_.flatMap(logResult, results))
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchOfac (deviceId, customer) {
|
function matchOfac(deviceId, customer) {
|
||||||
return Promise.resolve()
|
return Promise.resolve().then(() => {
|
||||||
.then(() => {
|
// Probably because we haven't asked for ID yet
|
||||||
// Probably because we haven't asked for ID yet
|
if (!_.isPlainObject(customer.idCardData)) {
|
||||||
if (!_.isPlainObject(customer.idCardData)) {
|
return true
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const nameParts = {
|
const nameParts = {
|
||||||
firstName: customer.idCardData.firstName,
|
firstName: customer.idCardData.firstName,
|
||||||
lastName: customer.idCardData.lastName
|
lastName: customer.idCardData.lastName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.some(_.isNil, _.values(nameParts))) {
|
if (_.some(_.isNil, _.values(nameParts))) {
|
||||||
logger.error(new Error(`Insufficient idCardData while matching OFAC for: ${customer.id}`))
|
logger.error(
|
||||||
return true
|
new Error(
|
||||||
}
|
`Insufficient idCardData while matching OFAC for: ${customer.id}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const birthDate = customer.idCardData.dateOfBirth
|
const birthDate = customer.idCardData.dateOfBirth
|
||||||
|
|
||||||
if (_.isNil(birthDate)) {
|
if (_.isNil(birthDate)) {
|
||||||
logger.error(new Error(`No birth date while matching OFAC for: ${customer.id}`))
|
logger.error(
|
||||||
return true
|
new Error(`No birth date while matching OFAC for: ${customer.id}`),
|
||||||
}
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
threshold: 0.85,
|
threshold: 0.85,
|
||||||
fullNameThreshold: 0.95,
|
fullNameThreshold: 0.95,
|
||||||
debug: false
|
debug: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = ofac.match(nameParts, birthDate, options)
|
const results = ofac.match(nameParts, birthDate, options)
|
||||||
|
|
||||||
return logSanctionsMatches(deviceId, customer, results)
|
return logSanctionsMatches(deviceId, customer, results).then(
|
||||||
.then(() => !_.isEmpty(results))
|
() => !_.isEmpty(results),
|
||||||
})
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOfac (deviceId, customer) {
|
function validateOfac(deviceId, customer) {
|
||||||
if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false)
|
if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false)
|
||||||
if (customer.sanctionsOverride === 'verified') return Promise.resolve(true)
|
if (customer.sanctionsOverride === 'verified') return Promise.resolve(true)
|
||||||
|
|
||||||
return matchOfac(deviceId, customer)
|
return matchOfac(deviceId, customer).then(didMatch => !didMatch)
|
||||||
.then(didMatch => !didMatch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validationPatch (deviceId, customer) {
|
function validationPatch(deviceId, customer) {
|
||||||
return validateOfac(deviceId, customer)
|
return validateOfac(deviceId, customer).then(sanctions =>
|
||||||
.then(sanctions =>
|
_.isNil(customer.sanctions) || customer.sanctions !== sanctions
|
||||||
_.isNil(customer.sanctions) || customer.sanctions !== sanctions ?
|
? { sanctions }
|
||||||
{ sanctions } :
|
: {},
|
||||||
{}
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {validationPatch}
|
module.exports = { validationPatch }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const uuid = require('uuid')
|
||||||
*
|
*
|
||||||
* @returns {object} Newly created compliance override
|
* @returns {object} Newly created compliance override
|
||||||
*/
|
*/
|
||||||
function add (complianceOverride) {
|
function add(complianceOverride) {
|
||||||
const sql = `insert into compliance_overrides
|
const sql = `insert into compliance_overrides
|
||||||
(id,
|
(id,
|
||||||
customer_id,
|
customer_id,
|
||||||
|
|
@ -25,7 +25,7 @@ function add (complianceOverride) {
|
||||||
complianceOverride.customerId,
|
complianceOverride.customerId,
|
||||||
complianceOverride.complianceType,
|
complianceOverride.complianceType,
|
||||||
complianceOverride.overrideBy,
|
complianceOverride.overrideBy,
|
||||||
complianceOverride.verification
|
complianceOverride.verification,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,43 +10,43 @@ const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HO
|
||||||
|
|
||||||
const anonymousCustomer = {
|
const anonymousCustomer = {
|
||||||
uuid: '47ac1184-8102-11e7-9079-8f13a7117867',
|
uuid: '47ac1184-8102-11e7-9079-8f13a7117867',
|
||||||
name: 'anonymous'
|
name: 'anonymous',
|
||||||
}
|
}
|
||||||
|
|
||||||
const CASH_UNIT_CAPACITY = {
|
const CASH_UNIT_CAPACITY = {
|
||||||
default: {
|
default: {
|
||||||
cashbox: 600,
|
cashbox: 600,
|
||||||
cassette: 500
|
cassette: 500,
|
||||||
},
|
},
|
||||||
douro: {
|
douro: {
|
||||||
cashbox: 600,
|
cashbox: 600,
|
||||||
cassette: 500
|
cassette: 500,
|
||||||
},
|
},
|
||||||
grandola: {
|
grandola: {
|
||||||
cashbox: 2000,
|
cashbox: 2000,
|
||||||
recycler: 2800
|
recycler: 2800,
|
||||||
},
|
},
|
||||||
aveiro: {
|
aveiro: {
|
||||||
cashbox: 1500,
|
cashbox: 1500,
|
||||||
recycler: 60,
|
recycler: 60,
|
||||||
cassette: 500
|
cassette: 500,
|
||||||
},
|
},
|
||||||
tejo: {
|
tejo: {
|
||||||
// TODO: add support for the different cashbox configuration in Tejo
|
// TODO: add support for the different cashbox configuration in Tejo
|
||||||
cashbox: 1000,
|
cashbox: 1000,
|
||||||
cassette: 500
|
cassette: 500,
|
||||||
},
|
},
|
||||||
gaia: {
|
gaia: {
|
||||||
cashbox: 600
|
cashbox: 600,
|
||||||
},
|
},
|
||||||
sintra: {
|
sintra: {
|
||||||
cashbox: 1000,
|
cashbox: 1000,
|
||||||
cassette: 500
|
cassette: 500,
|
||||||
},
|
},
|
||||||
gmuk1: {
|
gmuk1: {
|
||||||
cashbox: 2200,
|
cashbox: 2200,
|
||||||
cassette: 2000
|
cassette: 2000,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
||||||
|
|
@ -69,7 +69,7 @@ const WALLET_SCORE_THRESHOLD = 9
|
||||||
|
|
||||||
const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
||||||
NORMAL: 1,
|
NORMAL: 1,
|
||||||
SLOW: 3
|
SLOW: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -90,5 +90,5 @@ module.exports = {
|
||||||
WALLET_SCORE_THRESHOLD,
|
WALLET_SCORE_THRESHOLD,
|
||||||
RECEIPT,
|
RECEIPT,
|
||||||
PSQL_URL,
|
PSQL_URL,
|
||||||
BALANCE_FETCH_SPEED_MULTIPLIER
|
BALANCE_FETCH_SPEED_MULTIPLIER,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ const db = require('./db')
|
||||||
|
|
||||||
const getCustomerNotes = customerId => {
|
const getCustomerNotes = customerId => {
|
||||||
const sql = `SELECT * FROM customer_notes WHERE customer_id=$1`
|
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) => {
|
const createCustomerNote = (customerId, userId, title, content) => {
|
||||||
|
|
@ -27,5 +29,5 @@ module.exports = {
|
||||||
getCustomerNotes,
|
getCustomerNotes,
|
||||||
createCustomerNote,
|
createCustomerNote,
|
||||||
deleteCustomerNote,
|
deleteCustomerNote,
|
||||||
updateCustomerNote
|
updateCustomerNote,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,15 +1,15 @@
|
||||||
const db = require('../lib/db')
|
const db = require('../lib/db')
|
||||||
const logger = require('./logger')
|
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) {
|
DbMigrateStore.prototype.save = function (set, fn) {
|
||||||
let insertData = JSON.stringify({
|
let insertData = JSON.stringify({
|
||||||
lastRun: set.lastRun,
|
lastRun: set.lastRun,
|
||||||
migrations: set.migrations
|
migrations: set.migrations,
|
||||||
})
|
})
|
||||||
db.none(upsert, [insertData]).then(fn).catch(logger.error)
|
db.none(upsert, [insertData]).then(fn).catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,8 @@ const pgp = Pgp({
|
||||||
else if (e.query) {
|
else if (e.query) {
|
||||||
logger.error(e.query)
|
logger.error(e.query)
|
||||||
e.params && logger.error(e.params)
|
e.params && logger.error(e.params)
|
||||||
}
|
} else logger.error(err)
|
||||||
else logger.error(err)
|
},
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const db = pgp(PSQL_URL)
|
const db = pgp(PSQL_URL)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
const ph = require('./plugin-helper')
|
const ph = require('./plugin-helper')
|
||||||
|
|
||||||
function sendMessage (settings, rec) {
|
function sendMessage(settings, rec) {
|
||||||
return Promise.resolve()
|
return Promise.resolve().then(() => {
|
||||||
.then(() => {
|
const pluginCode =
|
||||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
settings.config.notifications_thirdParty_email || 'mailgun'
|
||||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
return plugin.sendMessage(account, rec)
|
return plugin.sendMessage(account, rec)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCustomerMessage (settings, rec) {
|
function sendCustomerMessage(settings, rec) {
|
||||||
return Promise.resolve()
|
return Promise.resolve().then(() => {
|
||||||
.then(() => {
|
const pluginCode =
|
||||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
settings.config.notifications_thirdParty_email || 'mailgun'
|
||||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
return plugin.sendMessage(account, rec)
|
return plugin.sendMessage(account, rec)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {sendMessage, sendCustomerMessage}
|
module.exports = { sendMessage, sendCustomerMessage }
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ const isProdMode = () => process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
|
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
|
||||||
|
|
||||||
function isRemoteNode (crypto) {
|
function isRemoteNode(crypto) {
|
||||||
return process.env[`${crypto.cryptoCode}_NODE_LOCATION`] === 'remote'
|
return process.env[`${crypto.cryptoCode}_NODE_LOCATION`] === 'remote'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemoteWallet (crypto) {
|
function isRemoteWallet(crypto) {
|
||||||
return process.env[`${crypto.cryptoCode}_WALLET_LOCATION`] === 'remote'
|
return process.env[`${crypto.cryptoCode}_WALLET_LOCATION`] === 'remote'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,5 +17,5 @@ module.exports = {
|
||||||
isDevMode,
|
isDevMode,
|
||||||
isProdMode,
|
isProdMode,
|
||||||
isRemoteNode,
|
isRemoteNode,
|
||||||
isRemoteWallet
|
isRemoteWallet,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const E = function (name) {
|
||||||
|
|
||||||
module.exports = E
|
module.exports = E
|
||||||
|
|
||||||
function register (errorName) {
|
function register(errorName) {
|
||||||
E[errorName] = E(errorName)
|
E[errorName] = E(errorName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
const subscriptions = {}
|
const subscriptions = {}
|
||||||
|
|
||||||
function subscribe (eventType, callback) {
|
function subscribe(eventType, callback) {
|
||||||
const id = uuid.v1()
|
const id = uuid.v1()
|
||||||
|
|
||||||
if (!subscriptions[eventType]) subscriptions[eventType] = {}
|
if (!subscriptions[eventType]) subscriptions[eventType] = {}
|
||||||
|
|
@ -15,15 +15,18 @@ function subscribe (eventType, callback) {
|
||||||
return {
|
return {
|
||||||
unsubscribe: () => {
|
unsubscribe: () => {
|
||||||
delete subscriptions[eventType][id]
|
delete subscriptions[eventType][id]
|
||||||
if (_.keys(subscriptions[eventType]).length === 0) delete subscriptions[eventType]
|
if (_.keys(subscriptions[eventType]).length === 0)
|
||||||
}
|
delete subscriptions[eventType]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function publish (eventType, arg) {
|
function publish(eventType, arg) {
|
||||||
if (!subscriptions[eventType]) return
|
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 }
|
module.exports = { subscribe, publish }
|
||||||
|
|
|
||||||
|
|
@ -6,71 +6,80 @@ const ccxt = require('./plugins/exchange/ccxt')
|
||||||
const mockExchange = require('./plugins/exchange/mock-exchange')
|
const mockExchange = require('./plugins/exchange/mock-exchange')
|
||||||
const accounts = require('./new-admin/config/accounts')
|
const accounts = require('./new-admin/config/accounts')
|
||||||
|
|
||||||
function lookupExchange (settings, cryptoCode) {
|
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
|
if (exchange === 'no-exchange') return null
|
||||||
return exchange
|
return exchange
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchExchange (settings, cryptoCode) {
|
function fetchExchange(settings, cryptoCode) {
|
||||||
return Promise.resolve()
|
return Promise.resolve().then(() => {
|
||||||
.then(() => {
|
const exchangeName = lookupExchange(settings, cryptoCode)
|
||||||
const exchangeName = lookupExchange(settings, cryptoCode)
|
if (exchangeName === 'mock-exchange')
|
||||||
if (exchangeName === 'mock-exchange') return { exchangeName, account: { currencyMarket: 'EUR' } }
|
return { exchangeName, account: { currencyMarket: 'EUR' } }
|
||||||
if (!exchangeName) throw new Error('No exchange set')
|
if (!exchangeName) throw new Error('No exchange set')
|
||||||
const account = settings.accounts[exchangeName]
|
const account = settings.accounts[exchangeName]
|
||||||
|
|
||||||
return { exchangeName, account }
|
return { exchangeName, account }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buy (settings, tradeEntry) {
|
function buy(settings, tradeEntry) {
|
||||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||||
return fetchExchange(settings, cryptoCode)
|
return fetchExchange(settings, cryptoCode).then(r => {
|
||||||
.then(r => {
|
if (r.exchangeName === 'mock-exchange') {
|
||||||
if (r.exchangeName === 'mock-exchange') {
|
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
|
||||||
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
|
}
|
||||||
}
|
return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName)
|
||||||
return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sell (settings, tradeEntry) {
|
function sell(settings, tradeEntry) {
|
||||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||||
return fetchExchange(settings, cryptoCode)
|
return fetchExchange(settings, cryptoCode).then(r => {
|
||||||
.then(r => {
|
if (r.exchangeName === 'mock-exchange') {
|
||||||
if (r.exchangeName === 'mock-exchange') {
|
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
|
||||||
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
|
}
|
||||||
}
|
return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName)
|
||||||
return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function active (settings, cryptoCode) {
|
function active(settings, cryptoCode) {
|
||||||
return !!lookupExchange(settings, cryptoCode)
|
return !!lookupExchange(settings, cryptoCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarkets () {
|
function getMarkets() {
|
||||||
const filterExchanges = _.filter(it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange')
|
const filterExchanges = _.filter(
|
||||||
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
|
it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange',
|
||||||
|
)
|
||||||
|
const availableExchanges = _.map(
|
||||||
|
it => it.code,
|
||||||
|
filterExchanges(accounts.ACCOUNT_LIST),
|
||||||
|
)
|
||||||
|
|
||||||
const fetchMarketForExchange = exchange =>
|
const fetchMarketForExchange = exchange =>
|
||||||
ccxt.getMarkets(exchange, ALL_CRYPTOS)
|
ccxt
|
||||||
|
.getMarkets(exchange, ALL_CRYPTOS)
|
||||||
.then(markets => ({ exchange, markets }))
|
.then(markets => ({ exchange, markets }))
|
||||||
.catch(error => ({
|
.catch(error => ({
|
||||||
exchange,
|
exchange,
|
||||||
markets: [],
|
markets: [],
|
||||||
error: error.message
|
error: error.message,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const transformToObject = _.reduce((acc, { exchange, markets }) => ({
|
const transformToObject = _.reduce(
|
||||||
...acc,
|
(acc, { exchange, markets }) => ({
|
||||||
[exchange]: markets
|
...acc,
|
||||||
}), {})
|
[exchange]: markets,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
const promises = _.map(fetchMarketForExchange, availableExchanges)
|
const promises = _.map(fetchMarketForExchange, availableExchanges)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises).then(transformToObject)
|
||||||
.then(transformToObject)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -78,5 +87,5 @@ module.exports = {
|
||||||
buy,
|
buy,
|
||||||
sell,
|
sell,
|
||||||
active,
|
active,
|
||||||
getMarkets
|
getMarkets,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,34 +7,52 @@ const T = require('./time')
|
||||||
|
|
||||||
const MAX_ROTATIONS = 5
|
const MAX_ROTATIONS = 5
|
||||||
|
|
||||||
const _getFiatRates = () => (
|
const _getFiatRates = () =>
|
||||||
axios.get('https://bitpay.com/api/rates')
|
axios.get('https://bitpay.com/api/rates').then(response => response.data)
|
||||||
.then(response => response.data)
|
|
||||||
)
|
|
||||||
|
|
||||||
const getFiatRates = mem(_getFiatRates, {
|
const getFiatRates = mem(_getFiatRates, {
|
||||||
maxAge: 6 * T.hours,
|
maxAge: 6 * T.hours,
|
||||||
cacheKey: () => ''
|
cacheKey: () => '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const API_QUEUE = [
|
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) {
|
function getBitPayFxRate(
|
||||||
return getFiatRates()
|
fiatCode,
|
||||||
.then(({ data: fxRates }) => {
|
defaultFiatMarket,
|
||||||
const defaultFiatRate = findCurrencyRates(fxRates, defaultFiatMarket, fiatCodeProperty, rateProperty)
|
fiatCodeProperty,
|
||||||
const fxRate = findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty).div(defaultFiatRate)
|
rateProperty,
|
||||||
return {
|
) {
|
||||||
fxRate
|
return getFiatRates().then(({ data: fxRates }) => {
|
||||||
}
|
const defaultFiatRate = findCurrencyRates(
|
||||||
})
|
fxRates,
|
||||||
|
defaultFiatMarket,
|
||||||
|
fiatCodeProperty,
|
||||||
|
rateProperty,
|
||||||
|
)
|
||||||
|
const fxRate = findCurrencyRates(
|
||||||
|
fxRates,
|
||||||
|
fiatCode,
|
||||||
|
fiatCodeProperty,
|
||||||
|
rateProperty,
|
||||||
|
).div(defaultFiatRate)
|
||||||
|
return {
|
||||||
|
fxRate,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCurrencyRates (fxRates, fiatCode, fiatCodeProperty, rateProperty) {
|
function findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty) {
|
||||||
const rates = _.find(_.matchesProperty(fiatCodeProperty, fiatCode), fxRates)
|
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())
|
return new BN(rates[rateProperty].toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,15 +64,20 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => {
|
||||||
|
|
||||||
if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`)
|
if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`)
|
||||||
|
|
||||||
return activeAPI(fiatCode, defaultFiatMarket, fiatCodeProperty, rateProperty)
|
return activeAPI(
|
||||||
.catch(() => {
|
fiatCode,
|
||||||
// Switch service
|
defaultFiatMarket,
|
||||||
const erroredService = API_QUEUE.shift()
|
fiatCodeProperty,
|
||||||
API_QUEUE.push(erroredService)
|
rateProperty,
|
||||||
if (retries >= MAX_ROTATIONS) throw new Error(`FOREX API error from ${erroredService.name}`)
|
).catch(() => {
|
||||||
|
// Switch service
|
||||||
|
const erroredService = API_QUEUE.shift()
|
||||||
|
API_QUEUE.push(erroredService)
|
||||||
|
if (retries >= MAX_ROTATIONS)
|
||||||
|
throw new Error(`FOREX API error from ${erroredService.name}`)
|
||||||
|
|
||||||
return getRate(++retries, fiatCode)
|
return getRate(++retries, fiatCode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getFiatRates, getRate }
|
module.exports = { getFiatRates, getRate }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ const nmd = require('nano-markdown')
|
||||||
const plugins = require('../plugins')
|
const plugins = require('../plugins')
|
||||||
const configManager = require('../new-config-manager')
|
const configManager = require('../new-config-manager')
|
||||||
const settingsLoader = require('../new-settings-loader')
|
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 state = require('../middlewares/state')
|
||||||
const { getMachine } = require('../machine-loader')
|
const { getMachine } = require('../machine-loader')
|
||||||
|
|
||||||
|
|
@ -14,23 +17,26 @@ const urlsToPing = [
|
||||||
`us.archive.ubuntu.com`,
|
`us.archive.ubuntu.com`,
|
||||||
`uk.archive.ubuntu.com`,
|
`uk.archive.ubuntu.com`,
|
||||||
`za.archive.ubuntu.com`,
|
`za.archive.ubuntu.com`,
|
||||||
`cn.archive.ubuntu.com`
|
`cn.archive.ubuntu.com`,
|
||||||
]
|
]
|
||||||
|
|
||||||
const speedtestFiles = [
|
const speedtestFiles = [
|
||||||
{
|
{
|
||||||
url: 'https://github.com/lamassu/speed-test-assets/raw/main/python-defaults_2.7.18-3.tar.gz',
|
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 =>
|
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(
|
const addOperatorInfo = addSmthInfo('operatorInfo', [
|
||||||
'operatorInfo',
|
'name',
|
||||||
['name', 'phone', 'email', 'website', 'companyNumber']
|
'phone',
|
||||||
)
|
'email',
|
||||||
|
'website',
|
||||||
|
'companyNumber',
|
||||||
|
])
|
||||||
|
|
||||||
const addReceiptInfo = receiptInfo => ret => {
|
const addReceiptInfo = receiptInfo => ret => {
|
||||||
if (!receiptInfo) return ret
|
if (!receiptInfo) return ret
|
||||||
|
|
@ -56,87 +62,90 @@ const addReceiptInfo = receiptInfo => ret => {
|
||||||
_.pick(fields),
|
_.pick(fields),
|
||||||
)(receiptInfo)
|
)(receiptInfo)
|
||||||
|
|
||||||
return (receiptInfo.paper || receiptInfo.sms) ?
|
return receiptInfo.paper || receiptInfo.sms
|
||||||
_.set('receiptInfo', receiptInfo, ret) :
|
? _.set('receiptInfo', receiptInfo, ret)
|
||||||
ret
|
: ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addMachineScreenOpts = smth =>
|
||||||
const addMachineScreenOpts = smth => _.update(
|
_.update(
|
||||||
'screenOptions',
|
'screenOptions',
|
||||||
_.flow(
|
_.flow(addSmthInfo('rates', ['active'])(smth.rates)),
|
||||||
addSmthInfo(
|
|
||||||
'rates',
|
|
||||||
[
|
|
||||||
'active'
|
|
||||||
]
|
|
||||||
)(smth.rates)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
/* TODO: Simplify this. */
|
/* TODO: Simplify this. */
|
||||||
const buildTriggers = allTriggers => {
|
const buildTriggers = allTriggers => {
|
||||||
const normalTriggers = []
|
const normalTriggers = []
|
||||||
const customTriggers = _.filter(o => {
|
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)
|
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
|
||||||
}, allTriggers)
|
}, allTriggers)
|
||||||
|
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.map(_.get('customInfoRequestId')),
|
_.map(_.get('customInfoRequestId')),
|
||||||
batchGetCustomInfoRequest
|
batchGetCustomInfoRequest,
|
||||||
)(customTriggers)
|
)(customTriggers).then(res => {
|
||||||
.then(res => {
|
res.forEach((details, index) => {
|
||||||
res.forEach((details, index) => {
|
// make sure we aren't attaching the details to the wrong trigger
|
||||||
// make sure we aren't attaching the details to the wrong trigger
|
if (customTriggers[index].customInfoRequestId !== details.id) return
|
||||||
if (customTriggers[index].customInfoRequestId !== details.id) return
|
customTriggers[index] = {
|
||||||
customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
|
...customTriggers[index],
|
||||||
})
|
customInfoRequest: details,
|
||||||
return [...normalTriggers, ...customTriggers]
|
}
|
||||||
})
|
})
|
||||||
|
return [...normalTriggers, ...customTriggers]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings, }) => {
|
const staticConfig = ({
|
||||||
const massageCoins = _.map(_.pick([
|
currentConfigVersion,
|
||||||
'batchable',
|
deviceId,
|
||||||
'cashInCommission',
|
deviceName,
|
||||||
'cashInFee',
|
pq,
|
||||||
'cashOutCommission',
|
settings,
|
||||||
'cashOutFee',
|
}) => {
|
||||||
'cryptoCode',
|
const massageCoins = _.map(
|
||||||
'cryptoCodeDisplay',
|
_.pick([
|
||||||
'cryptoNetwork',
|
'batchable',
|
||||||
'cryptoUnits',
|
'cashInCommission',
|
||||||
'display',
|
'cashInFee',
|
||||||
'minimumTx',
|
'cashOutCommission',
|
||||||
'isCashInOnly'
|
'cashOutFee',
|
||||||
]))
|
'cryptoCode',
|
||||||
|
'cryptoCodeDisplay',
|
||||||
|
'cryptoNetwork',
|
||||||
|
'cryptoUnits',
|
||||||
|
'display',
|
||||||
|
'minimumTx',
|
||||||
|
'isCashInOnly',
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
const staticConf = _.flow(
|
const staticConf = _.flow(
|
||||||
_.pick([
|
_.pick(['coins', 'configVersion', 'timezone', 'screenOptions']),
|
||||||
'coins',
|
|
||||||
'configVersion',
|
|
||||||
'timezone',
|
|
||||||
'screenOptions'
|
|
||||||
]),
|
|
||||||
_.update('coins', massageCoins),
|
_.update('coins', massageCoins),
|
||||||
_.set('serverVersion', VERSION),
|
_.set('serverVersion', VERSION),
|
||||||
)(pq)
|
)(pq)
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
!!configManager.getCompliance(settings.config).enablePaperWalletOnly,
|
!!configManager.getCompliance(settings.config).enablePaperWalletOnly,
|
||||||
configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config),
|
configManager.getTriggersAutomation(
|
||||||
|
getCustomInfoRequests(true),
|
||||||
|
settings.config,
|
||||||
|
),
|
||||||
buildTriggers(configManager.getTriggers(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.getLocale(deviceId, settings.config),
|
||||||
configManager.getOperatorInfo(settings.config),
|
configManager.getOperatorInfo(settings.config),
|
||||||
configManager.getReceipt(settings.config),
|
configManager.getReceipt(settings.config),
|
||||||
configManager.getAllMachineScreenOpts(settings.config),
|
configManager.getAllMachineScreenOpts(settings.config),
|
||||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||||
getMachine(deviceId, currentConfigVersion),
|
getMachine(deviceId, currentConfigVersion),
|
||||||
configManager.getCustomerAuthenticationMethod(settings.config)
|
configManager.getCustomerAuthenticationMethod(settings.config),
|
||||||
])
|
]).then(
|
||||||
.then(([
|
([
|
||||||
enablePaperWalletOnly,
|
enablePaperWalletOnly,
|
||||||
triggersAutomation,
|
triggersAutomation,
|
||||||
triggers,
|
triggers,
|
||||||
|
|
@ -149,88 +158,121 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
{ numberOfCassettes, numberOfRecyclers },
|
{ numberOfCassettes, numberOfRecyclers },
|
||||||
customerAuthentication,
|
customerAuthentication,
|
||||||
]) =>
|
]) =>
|
||||||
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
currentConfigVersion && currentConfigVersion >= staticConf.configVersion
|
||||||
null :
|
? null
|
||||||
_.flow(
|
: _.flow(
|
||||||
_.assign({
|
_.assign({
|
||||||
enablePaperWalletOnly,
|
enablePaperWalletOnly,
|
||||||
triggersAutomation,
|
triggersAutomation,
|
||||||
triggers,
|
triggers,
|
||||||
hasLightning,
|
hasLightning,
|
||||||
localeInfo: {
|
localeInfo: {
|
||||||
country: localeInfo.country,
|
country: localeInfo.country,
|
||||||
languages: localeInfo.languages,
|
languages: localeInfo.languages,
|
||||||
fiatCode: localeInfo.fiatCurrency
|
fiatCode: localeInfo.fiatCurrency,
|
||||||
},
|
},
|
||||||
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfRecyclers },
|
machineInfo: {
|
||||||
twoWayMode,
|
deviceId,
|
||||||
customerAuthentication,
|
deviceName,
|
||||||
speedtestFiles,
|
numberOfCassettes,
|
||||||
urlsToPing,
|
numberOfRecyclers,
|
||||||
}),
|
},
|
||||||
addOperatorInfo(operatorInfo),
|
twoWayMode,
|
||||||
addReceiptInfo(receiptInfo),
|
customerAuthentication,
|
||||||
addMachineScreenOpts(machineScreenOpts)
|
speedtestFiles,
|
||||||
)(staticConf))
|
urlsToPing,
|
||||||
|
}),
|
||||||
|
addOperatorInfo(operatorInfo),
|
||||||
|
addReceiptInfo(receiptInfo),
|
||||||
|
addMachineScreenOpts(machineScreenOpts),
|
||||||
|
)(staticConf),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const setZeroConfLimit = config => coin =>
|
const setZeroConfLimit = config => coin =>
|
||||||
_.set(
|
_.set(
|
||||||
'zeroConfLimit',
|
'zeroConfLimit',
|
||||||
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit ?? 0,
|
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 =>
|
const massageCassettes = cassettes =>
|
||||||
cassettes ?
|
cassettes
|
||||||
_.flow(
|
? _.flow(
|
||||||
cassettes => _.set('physical', _.get('cassettes', cassettes), cassettes),
|
cassettes =>
|
||||||
cassettes => _.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
|
_.set('physical', _.get('cassettes', cassettes), cassettes),
|
||||||
_.unset('cassettes'),
|
cassettes =>
|
||||||
_.unset('virtualCassettes')
|
_.set('virtual', _.get('virtualCassettes', cassettes), cassettes),
|
||||||
)(cassettes) :
|
_.unset('cassettes'),
|
||||||
null
|
_.unset('virtualCassettes'),
|
||||||
|
)(cassettes)
|
||||||
|
: null
|
||||||
|
|
||||||
const massageRecyclers = recyclers =>
|
const massageRecyclers = recyclers =>
|
||||||
recyclers ?
|
recyclers
|
||||||
_.flow(
|
? _.flow(
|
||||||
recyclers => _.set('physical', _.get('recyclers', recyclers), recyclers),
|
recyclers =>
|
||||||
recyclers => _.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
|
_.set('physical', _.get('recyclers', recyclers), recyclers),
|
||||||
_.unset('recyclers'),
|
recyclers =>
|
||||||
_.unset('virtualRecyclers')
|
_.set('virtual', _.get('virtualRecyclers', recyclers), recyclers),
|
||||||
)(recyclers) :
|
_.unset('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(
|
const res = _.flow(
|
||||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'recyclers', 'coins', 'rates']),
|
_.pick([
|
||||||
|
'areThereAvailablePromoCodes',
|
||||||
|
'balances',
|
||||||
|
'cassettes',
|
||||||
|
'recyclers',
|
||||||
|
'coins',
|
||||||
|
'rates',
|
||||||
|
]),
|
||||||
|
|
||||||
_.update('cassettes', massageCassettes),
|
_.update('cassettes', massageCassettes),
|
||||||
|
|
||||||
_.update('recyclers', massageRecyclers),
|
_.update('recyclers', massageRecyclers),
|
||||||
|
|
||||||
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
||||||
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
|
_.update(
|
||||||
|
'coins',
|
||||||
|
_.map(({ cryptoCode, rates }) => [cryptoCode, rates]),
|
||||||
|
),
|
||||||
|
|
||||||
/* [{ cryptoCode: balance }, ...] => [[cryptoCode, { balance }], ...] */
|
/* [{ cryptoCode: balance }, ...] => [[cryptoCode, { balance }], ...] */
|
||||||
_.update('balances', _.flow(
|
_.update(
|
||||||
_.toPairs,
|
'balances',
|
||||||
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }])
|
_.flow(
|
||||||
)),
|
_.toPairs,
|
||||||
|
_.map(([cryptoCode, balance]) => [cryptoCode, { balance }]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
/* Group the separate objects by cryptoCode */
|
/* Group the separate objects by cryptoCode */
|
||||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||||
({ areThereAvailablePromoCodes, balances, cassettes, recyclers, coins, rates }) => ({
|
({
|
||||||
|
areThereAvailablePromoCodes,
|
||||||
|
balances,
|
||||||
|
cassettes,
|
||||||
|
recyclers,
|
||||||
|
coins,
|
||||||
|
rates,
|
||||||
|
}) => ({
|
||||||
areThereAvailablePromoCodes,
|
areThereAvailablePromoCodes,
|
||||||
cassettes,
|
cassettes,
|
||||||
recyclers,
|
recyclers,
|
||||||
coins: _.flow(
|
coins: _.flow(
|
||||||
_.reduce(
|
_.reduce(
|
||||||
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
||||||
rates
|
rates,
|
||||||
),
|
),
|
||||||
|
|
||||||
/* { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } => [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] */
|
/* { 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)),
|
_.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 */
|
/* 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))
|
_.filter(coin =>
|
||||||
)(_.concat(balances, coins))
|
['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(
|
||||||
|
it => it in coin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)(_.concat(balances, coins)),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
_.update('coins', _.map(setZeroConfLimit(settings.config))),
|
_.update('coins', _.map(setZeroConfLimit(settings.config))),
|
||||||
_.set('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
|
_.set('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
|
||||||
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
|
_.set(
|
||||||
_.set('restartServices', !!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid),
|
'shutdown',
|
||||||
_.set('emptyUnit', !!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid),
|
!!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid,
|
||||||
_.set('refillUnit', !!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid),
|
),
|
||||||
_.set('diagnostics', !!pid && state.diagnostics?.[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)
|
)(pq)
|
||||||
|
|
||||||
// Clean up the state middleware and prevent commands from being issued more than once
|
// 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
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configs = (
|
||||||
const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) =>
|
parent,
|
||||||
|
{ currentConfigVersion },
|
||||||
|
{ deviceId, deviceName, operatorId, pid, settings },
|
||||||
|
) =>
|
||||||
plugins(settings, deviceId)
|
plugins(settings, deviceId)
|
||||||
.pollQueries()
|
.pollQueries()
|
||||||
.then(pq => ({
|
.then(pq => ({
|
||||||
|
|
@ -290,15 +354,17 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const massageTerms = terms =>
|
||||||
const massageTerms = terms => (terms.active && terms.text) ? ({
|
terms.active && terms.text
|
||||||
tcPhoto: Boolean(terms.tcPhoto),
|
? {
|
||||||
delay: Boolean(terms.delay),
|
tcPhoto: Boolean(terms.tcPhoto),
|
||||||
title: terms.title,
|
delay: Boolean(terms.delay),
|
||||||
text: nmd(terms.text),
|
title: terms.title,
|
||||||
accept: terms.acceptButtonText,
|
text: nmd(terms.text),
|
||||||
cancel: terms.cancelButtonText,
|
accept: terms.acceptButtonText,
|
||||||
}) : null
|
cancel: terms.cancelButtonText,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The type of the result of `configManager.getTermsConditions()` is more or
|
* 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
|
* If the `hash` differs from `currentHash` then everything is resent (to
|
||||||
* simplify machine implementation).
|
* simplify machine implementation).
|
||||||
*/
|
*/
|
||||||
const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settings }, info) => {
|
const terms = (parent, { currentConfigVersion, currentHash }, { settings }) => {
|
||||||
const isNone = x => _.isNil(x) || _.isEmpty(x)
|
const isNone = x => _.isNil(x) || _.isEmpty(x)
|
||||||
|
|
||||||
let latestTerms = configManager.getTermsConditions(settings.config)
|
let latestTerms = configManager.getTermsConditions(settings.config)
|
||||||
|
|
@ -342,17 +408,22 @@ const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settin
|
||||||
const isHashNew = hash !== currentHash
|
const isHashNew = hash !== currentHash
|
||||||
const text = isHashNew ? latestTerms.text : null
|
const text = isHashNew ? latestTerms.text : null
|
||||||
|
|
||||||
return settingsLoader.fetchCurrentConfigVersion()
|
return settingsLoader
|
||||||
|
.fetchCurrentConfigVersion()
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
.then(configVersion => isHashNew || _.isNil(currentConfigVersion) || currentConfigVersion < configVersion)
|
.then(
|
||||||
.then(isVersionNew => isVersionNew ? _.omit(['text'], latestTerms) : null)
|
configVersion =>
|
||||||
|
isHashNew ||
|
||||||
|
_.isNil(currentConfigVersion) ||
|
||||||
|
currentConfigVersion < configVersion,
|
||||||
|
)
|
||||||
|
.then(isVersionNew => (isVersionNew ? _.omit(['text'], latestTerms) : null))
|
||||||
.then(details => ({ hash, details, text }))
|
.then(details => ({ hash, details, text }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Query: {
|
Query: {
|
||||||
configs,
|
configs,
|
||||||
terms,
|
terms,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ const { ApolloServer } = require('@apollo/server')
|
||||||
const devMode = !!require('minimist')(process.argv.slice(2)).dev
|
const devMode = !!require('minimist')(process.argv.slice(2)).dev
|
||||||
|
|
||||||
const context = ({ req, res }) => ({
|
const context = ({ req, res }) => ({
|
||||||
deviceId: req.deviceId, /* lib/middlewares/populateDeviceId.js */
|
deviceId: req.deviceId /* lib/middlewares/populateDeviceId.js */,
|
||||||
deviceName: req.deviceName, /* lib/middlewares/authorize.js */
|
deviceName: req.deviceName /* lib/middlewares/authorize.js */,
|
||||||
operatorId: res.locals.operatorId, /* lib/middlewares/operatorId.js */
|
operatorId: res.locals.operatorId /* lib/middlewares/operatorId.js */,
|
||||||
pid: req.query.pid,
|
pid: req.query.pid,
|
||||||
settings: req.settings, /* lib/middlewares/populateSettings.js */
|
settings: req.settings /* lib/middlewares/populateSettings.js */,
|
||||||
})
|
})
|
||||||
|
|
||||||
const graphQLServer = new ApolloServer({
|
const graphQLServer = new ApolloServer({
|
||||||
|
|
@ -21,7 +21,7 @@ const graphQLServer = new ApolloServer({
|
||||||
return error
|
return error
|
||||||
},
|
},
|
||||||
includeStacktraceInErrorResponses: devMode,
|
includeStacktraceInErrorResponses: devMode,
|
||||||
logger
|
logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = { graphQLServer, context }
|
module.exports = { graphQLServer, context }
|
||||||
|
|
@ -1,234 +1,234 @@
|
||||||
const gql = require('graphql-tag')
|
const gql = require('graphql-tag')
|
||||||
|
|
||||||
module.exports = gql`
|
module.exports = gql`
|
||||||
type Coin {
|
type Coin {
|
||||||
cryptoCode: String!
|
cryptoCode: String!
|
||||||
cryptoCodeDisplay: String!
|
cryptoCodeDisplay: String!
|
||||||
display: String!
|
display: String!
|
||||||
minimumTx: String!
|
minimumTx: String!
|
||||||
cashInFee: String!
|
cashInFee: String!
|
||||||
cashOutFee: String!
|
cashOutFee: String!
|
||||||
cashInCommission: String!
|
cashInCommission: String!
|
||||||
cashOutCommission: String!
|
cashOutCommission: String!
|
||||||
cryptoNetwork: String!
|
cryptoNetwork: String!
|
||||||
cryptoUnits: String!
|
cryptoUnits: String!
|
||||||
batchable: Boolean!
|
batchable: Boolean!
|
||||||
isCashInOnly: Boolean!
|
isCashInOnly: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocaleInfo {
|
type LocaleInfo {
|
||||||
country: String!
|
country: String!
|
||||||
fiatCode: String!
|
fiatCode: String!
|
||||||
languages: [String!]!
|
languages: [String!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type OperatorInfo {
|
type OperatorInfo {
|
||||||
name: String!
|
name: String!
|
||||||
phone: String!
|
phone: String!
|
||||||
email: String!
|
email: String!
|
||||||
website: String!
|
website: String!
|
||||||
companyNumber: String!
|
companyNumber: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachineInfo {
|
type MachineInfo {
|
||||||
deviceId: String! @deprecated(reason: "unused by the machine")
|
deviceId: String! @deprecated(reason: "unused by the machine")
|
||||||
deviceName: String
|
deviceName: String
|
||||||
numberOfCassettes: Int
|
numberOfCassettes: Int
|
||||||
numberOfRecyclers: Int
|
numberOfRecyclers: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceiptInfo {
|
type ReceiptInfo {
|
||||||
paper: Boolean!
|
paper: Boolean!
|
||||||
automaticPrint: Boolean!
|
automaticPrint: Boolean!
|
||||||
sms: Boolean!
|
sms: Boolean!
|
||||||
operatorWebsite: Boolean!
|
operatorWebsite: Boolean!
|
||||||
operatorEmail: Boolean!
|
operatorEmail: Boolean!
|
||||||
operatorPhone: Boolean!
|
operatorPhone: Boolean!
|
||||||
companyNumber: Boolean!
|
companyNumber: Boolean!
|
||||||
machineLocation: Boolean!
|
machineLocation: Boolean!
|
||||||
customerNameOrPhoneNumber: Boolean!
|
customerNameOrPhoneNumber: Boolean!
|
||||||
exchangeRate: Boolean!
|
exchangeRate: Boolean!
|
||||||
addressQRCode: Boolean!
|
addressQRCode: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type MachineScreenOptions {
|
type MachineScreenOptions {
|
||||||
rates: RateScreenOptions!
|
rates: RateScreenOptions!
|
||||||
}
|
}
|
||||||
|
|
||||||
type RateScreenOptions {
|
type RateScreenOptions {
|
||||||
active: Boolean!
|
active: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpeedtestFile {
|
type SpeedtestFile {
|
||||||
url: String!
|
url: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TriggerAutomationType {
|
enum TriggerAutomationType {
|
||||||
Automatic
|
Automatic
|
||||||
Manual
|
Manual
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomTriggersAutomation {
|
type CustomTriggersAutomation {
|
||||||
id: ID!
|
id: ID!
|
||||||
type: TriggerAutomationType!
|
type: TriggerAutomationType!
|
||||||
}
|
}
|
||||||
|
|
||||||
type TriggersAutomation {
|
type TriggersAutomation {
|
||||||
sanctions: TriggerAutomationType!
|
sanctions: TriggerAutomationType!
|
||||||
idCardPhoto: TriggerAutomationType!
|
idCardPhoto: TriggerAutomationType!
|
||||||
idCardData: TriggerAutomationType!
|
idCardData: TriggerAutomationType!
|
||||||
facephoto: TriggerAutomationType!
|
facephoto: TriggerAutomationType!
|
||||||
usSsn: TriggerAutomationType!
|
usSsn: TriggerAutomationType!
|
||||||
custom: [CustomTriggersAutomation]!
|
custom: [CustomTriggersAutomation]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomScreen {
|
type CustomScreen {
|
||||||
text: String!
|
text: String!
|
||||||
title: String!
|
title: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomInput {
|
type CustomInput {
|
||||||
type: String!
|
type: String!
|
||||||
constraintType: String!
|
constraintType: String!
|
||||||
label1: String
|
label1: String
|
||||||
label2: String
|
label2: String
|
||||||
choiceList: [String]
|
choiceList: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomRequest {
|
type CustomRequest {
|
||||||
name: String!
|
name: String!
|
||||||
input: CustomInput!
|
input: CustomInput!
|
||||||
screen1: CustomScreen!
|
screen1: CustomScreen!
|
||||||
screen2: CustomScreen!
|
screen2: CustomScreen!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomInfoRequest {
|
type CustomInfoRequest {
|
||||||
id: String!
|
id: String!
|
||||||
enabled: Boolean!
|
enabled: Boolean!
|
||||||
customRequest: CustomRequest!
|
customRequest: CustomRequest!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Trigger {
|
type Trigger {
|
||||||
id: String!
|
id: String!
|
||||||
direction: String!
|
direction: String!
|
||||||
requirement: String!
|
requirement: String!
|
||||||
triggerType: String!
|
triggerType: String!
|
||||||
|
|
||||||
suspensionDays: Float
|
suspensionDays: Float
|
||||||
threshold: Int
|
threshold: Int
|
||||||
thresholdDays: Int
|
thresholdDays: Int
|
||||||
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
|
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
|
||||||
customInfoRequest: CustomInfoRequest
|
customInfoRequest: CustomInfoRequest
|
||||||
externalService: String
|
externalService: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type TermsDetails {
|
type TermsDetails {
|
||||||
tcPhoto: Boolean!
|
tcPhoto: Boolean!
|
||||||
delay: Boolean!
|
delay: Boolean!
|
||||||
title: String!
|
title: String!
|
||||||
accept: String!
|
accept: String!
|
||||||
cancel: String!
|
cancel: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Terms {
|
type Terms {
|
||||||
hash: String!
|
hash: String!
|
||||||
text: String
|
text: String
|
||||||
details: TermsDetails
|
details: TermsDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CustomerAuthentication {
|
enum CustomerAuthentication {
|
||||||
EMAIL
|
EMAIL
|
||||||
SMS
|
SMS
|
||||||
}
|
}
|
||||||
|
|
||||||
type StaticConfig {
|
type StaticConfig {
|
||||||
configVersion: Int!
|
configVersion: Int!
|
||||||
|
|
||||||
coins: [Coin!]!
|
coins: [Coin!]!
|
||||||
enablePaperWalletOnly: Boolean!
|
enablePaperWalletOnly: Boolean!
|
||||||
hasLightning: Boolean!
|
hasLightning: Boolean!
|
||||||
serverVersion: String!
|
serverVersion: String!
|
||||||
timezone: Int!
|
timezone: Int!
|
||||||
twoWayMode: Boolean!
|
twoWayMode: Boolean!
|
||||||
customerAuthentication: CustomerAuthentication!
|
customerAuthentication: CustomerAuthentication!
|
||||||
|
|
||||||
localeInfo: LocaleInfo!
|
localeInfo: LocaleInfo!
|
||||||
operatorInfo: OperatorInfo
|
operatorInfo: OperatorInfo
|
||||||
machineInfo: MachineInfo!
|
machineInfo: MachineInfo!
|
||||||
receiptInfo: ReceiptInfo
|
receiptInfo: ReceiptInfo
|
||||||
screenOptions: MachineScreenOptions
|
screenOptions: MachineScreenOptions
|
||||||
|
|
||||||
speedtestFiles: [SpeedtestFile!]!
|
speedtestFiles: [SpeedtestFile!]!
|
||||||
urlsToPing: [String!]!
|
urlsToPing: [String!]!
|
||||||
|
|
||||||
triggersAutomation: TriggersAutomation!
|
triggersAutomation: TriggersAutomation!
|
||||||
triggers: [Trigger!]!
|
triggers: [Trigger!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicCoinValues {
|
type DynamicCoinValues {
|
||||||
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
|
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
|
||||||
# However, it can be used to generate the cache key, if we ever move to an
|
# However, it can be used to generate the cache key, if we ever move to an
|
||||||
# actual caching mechanism.
|
# actual caching mechanism.
|
||||||
#timestamp: String!
|
#timestamp: String!
|
||||||
|
|
||||||
cryptoCode: String!
|
cryptoCode: String!
|
||||||
balance: String!
|
balance: String!
|
||||||
|
|
||||||
# Raw rates
|
# Raw rates
|
||||||
ask: String!
|
ask: String!
|
||||||
bid: String!
|
bid: String!
|
||||||
|
|
||||||
# Rates with commissions applied
|
# Rates with commissions applied
|
||||||
cashIn: String!
|
cashIn: String!
|
||||||
cashOut: String!
|
cashOut: String!
|
||||||
|
|
||||||
zeroConfLimit: Int!
|
zeroConfLimit: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysicalCassette {
|
type PhysicalCassette {
|
||||||
name: String!
|
name: String!
|
||||||
denomination: Int!
|
denomination: Int!
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysicalRecycler {
|
type PhysicalRecycler {
|
||||||
name: String!
|
name: String!
|
||||||
number: Int!
|
number: Int!
|
||||||
denomination: Int!
|
denomination: Int!
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cassettes {
|
type Cassettes {
|
||||||
physical: [PhysicalCassette!]!
|
physical: [PhysicalCassette!]!
|
||||||
virtual: [Int!]!
|
virtual: [Int!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Recyclers {
|
type Recyclers {
|
||||||
physical: [PhysicalRecycler!]!
|
physical: [PhysicalRecycler!]!
|
||||||
virtual: [Int!]!
|
virtual: [Int!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicConfig {
|
type DynamicConfig {
|
||||||
areThereAvailablePromoCodes: Boolean!
|
areThereAvailablePromoCodes: Boolean!
|
||||||
cassettes: Cassettes
|
cassettes: Cassettes
|
||||||
recyclers: Recyclers
|
recyclers: Recyclers
|
||||||
coins: [DynamicCoinValues!]!
|
coins: [DynamicCoinValues!]!
|
||||||
reboot: Boolean!
|
reboot: Boolean!
|
||||||
shutdown: Boolean!
|
shutdown: Boolean!
|
||||||
restartServices: Boolean!
|
restartServices: Boolean!
|
||||||
emptyUnit: Boolean!
|
emptyUnit: Boolean!
|
||||||
refillUnit: Boolean!
|
refillUnit: Boolean!
|
||||||
diagnostics: Boolean!
|
diagnostics: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Configs {
|
type Configs {
|
||||||
static: StaticConfig
|
static: StaticConfig
|
||||||
dynamic: DynamicConfig!
|
dynamic: DynamicConfig!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
configs(currentConfigVersion: Int): Configs!
|
configs(currentConfigVersion: Int): Configs!
|
||||||
terms(currentHash: String, currentConfigVersion: Int): Terms
|
terms(currentHash: String, currentConfigVersion: Int): Terms
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,27 @@ const uuid = require('uuid')
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
|
||||||
function createHardwareCredential (userID, credentialData) {
|
function createHardwareCredential(userID, credentialData) {
|
||||||
const sql = `INSERT INTO hardware_credentials (id, user_id, data) VALUES ($1, $2, $3)`
|
const sql = `INSERT INTO hardware_credentials (id, user_id, data) VALUES ($1, $2, $3)`
|
||||||
return db.none(sql, [uuid.v4(), userID, credentialData])
|
return db.none(sql, [uuid.v4(), userID, credentialData])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHardwareCredentials () {
|
function getHardwareCredentials() {
|
||||||
const sql = `SELECT * FROM hardware_credentials`
|
const sql = `SELECT * FROM hardware_credentials`
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHardwareCredentialsByUserId (userID) {
|
function getHardwareCredentialsByUserId(userID) {
|
||||||
const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1`
|
const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1`
|
||||||
return db.any(sql, [userID])
|
return db.any(sql, [userID])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserByUserHandle (userHandle) {
|
function getUserByUserHandle(userHandle) {
|
||||||
const sql = `SELECT users.id, users.username, users.role FROM users INNER JOIN hardware_credentials hc ON users.id=hc.user_id WHERE data->>'userHandle'=$1::jsonb::text`
|
const sql = `SELECT users.id, users.username, users.role FROM users INNER JOIN hardware_credentials hc ON users.id=hc.user_id WHERE data->>'userHandle'=$1::jsonb::text`
|
||||||
return db.oneOrNone(sql, [userHandle])
|
return db.oneOrNone(sql, [userHandle])
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHardwareCredential (credential) {
|
function updateHardwareCredential(credential) {
|
||||||
const sql = `UPDATE hardware_credentials SET last_used=now(), data=$1 WHERE id=$2`
|
const sql = `UPDATE hardware_credentials SET last_used=now(), data=$1 WHERE id=$2`
|
||||||
return db.none(sql, [credential.data, credential.id])
|
return db.none(sql, [credential.data, credential.id])
|
||||||
}
|
}
|
||||||
|
|
@ -32,5 +32,5 @@ module.exports = {
|
||||||
getHardwareCredentials,
|
getHardwareCredentials,
|
||||||
getHardwareCredentialsByUserId,
|
getHardwareCredentialsByUserId,
|
||||||
getUserByUserHandle,
|
getUserByUserHandle,
|
||||||
updateHardwareCredential
|
updateHardwareCredential,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,47 @@ const configManager = require('./new-config-manager')
|
||||||
const ph = require('./plugin-helper')
|
const ph = require('./plugin-helper')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
function fetch (settings, cryptoCode) {
|
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()
|
if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve()
|
||||||
|
|
||||||
const layer2 = ph.load(ph.LAYER2, plugin)
|
const layer2 = ph.load(ph.LAYER2, plugin)
|
||||||
const account = settings.accounts[plugin]
|
const account = settings.accounts[plugin]
|
||||||
|
|
||||||
return Promise.resolve({layer2, account})
|
return Promise.resolve({ layer2, account })
|
||||||
}
|
}
|
||||||
|
|
||||||
function newAddress (settings, info) {
|
function newAddress(settings, info) {
|
||||||
return fetch(settings, info.cryptoCode)
|
return fetch(settings, info.cryptoCode).then(r => {
|
||||||
.then(r => {
|
if (!r) return
|
||||||
if (!r) return
|
return r.layer2.newAddress(r.account, info)
|
||||||
return r.layer2.newAddress(r.account, info)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatus (settings, tx) {
|
function getStatus(settings, tx) {
|
||||||
const toAddress = tx.layer2Address
|
const toAddress = tx.layer2Address
|
||||||
if (!toAddress) return Promise.resolve({status: 'notSeen'})
|
if (!toAddress) return Promise.resolve({ status: 'notSeen' })
|
||||||
|
|
||||||
return fetch(settings, tx.cryptoCode)
|
return fetch(settings, tx.cryptoCode).then(r => {
|
||||||
.then(r => {
|
if (!r) return { status: 'notSeen' }
|
||||||
if (!r) return {status: 'notSeen'}
|
return r.layer2.getStatus(
|
||||||
return r.layer2.getStatus(r.account, toAddress, tx.cryptoAtoms, tx.cryptoCode)
|
r.account,
|
||||||
})
|
toAddress,
|
||||||
|
tx.cryptoAtoms,
|
||||||
|
tx.cryptoCode,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cryptoNetwork (settings, 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 layer2 = ph.load(ph.LAYER2, plugin)
|
||||||
const account = settings.accounts[plugin]
|
const account = settings.accounts[plugin]
|
||||||
|
|
||||||
|
|
@ -41,7 +50,7 @@ function cryptoNetwork (settings, cryptoCode) {
|
||||||
return layer2.cryptoNetwork(account, cryptoCode)
|
return layer2.cryptoNetwork(account, cryptoCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLayer2Address (address) {
|
function isLayer2Address(address) {
|
||||||
return address.split(':').length >= 2
|
return address.split(':').length >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,5 +58,5 @@ module.exports = {
|
||||||
isLayer2Address,
|
isLayer2Address,
|
||||||
newAddress,
|
newAddress,
|
||||||
getStatus,
|
getStatus,
|
||||||
cryptoNetwork
|
cryptoNetwork,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,40 +7,42 @@ const LOG_LEVEL = process.env.LOG_LEVEL
|
||||||
const logger = new winston.Logger({
|
const logger = new winston.Logger({
|
||||||
level: LOG_LEVEL,
|
level: LOG_LEVEL,
|
||||||
transports: [
|
transports: [
|
||||||
new (winston.transports.Console)({
|
new winston.transports.Console({
|
||||||
timestamp: true,
|
timestamp: true,
|
||||||
colorize: true,
|
colorize: true,
|
||||||
handleExceptions: true,
|
handleExceptions: true,
|
||||||
humanReadableUnhandledException: true
|
humanReadableUnhandledException: true,
|
||||||
}),
|
}),
|
||||||
new Postgres({
|
new Postgres({
|
||||||
connectionString: PSQL_URL,
|
connectionString: PSQL_URL,
|
||||||
tableName: 'server_logs',
|
tableName: 'server_logs',
|
||||||
handleExceptions: true,
|
handleExceptions: true,
|
||||||
humanReadableUnhandledException: true
|
humanReadableUnhandledException: true,
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
rewriters: [
|
rewriters: [
|
||||||
(...[,, meta]) => {
|
(...[, , meta]) => {
|
||||||
if (meta.isAxiosError) {
|
if (meta.isAxiosError) {
|
||||||
return {
|
return {
|
||||||
message: meta.message,
|
message: meta.message,
|
||||||
status: meta.response?.status,
|
status: meta.response?.status,
|
||||||
data: meta.response?.data,
|
data: meta.response?.data,
|
||||||
url: meta.config?.url,
|
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 = {
|
logger.stream = {
|
||||||
write: message => {
|
write: message => {
|
||||||
logger.info(message.trim())
|
logger.info(message.trim())
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = logger
|
module.exports = logger
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const logger = require('./logger')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
const getMachineName = require('./machine-loader').getMachineName
|
const getMachineName = require('./machine-loader').getMachineName
|
||||||
const NUM_RESULTS = 500
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest log's timestamp
|
* Get the latest log's timestamp
|
||||||
|
|
@ -20,12 +19,15 @@ const NUM_RESULTS = 500
|
||||||
*
|
*
|
||||||
* @returns {date} Last timestamp
|
* @returns {date} Last timestamp
|
||||||
*/
|
*/
|
||||||
function getLastSeen (deviceId) {
|
function getLastSeen(deviceId) {
|
||||||
const sql = `select id, timestamp, serial from logs
|
const sql = `select id, timestamp, serial from logs
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
order by timestamp desc, serial desc limit 1`
|
order by timestamp desc, serial desc limit 1`
|
||||||
return db.oneOrNone(sql, [deviceId])
|
return db
|
||||||
.then(log => log ? {timestamp: log.timestamp, serial: log.serial, id: log.id} : null)
|
.oneOrNone(sql, [deviceId])
|
||||||
|
.then(log =>
|
||||||
|
log ? { timestamp: log.timestamp, serial: log.serial, id: log.id } : null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,10 +42,11 @@ function getLastSeen (deviceId) {
|
||||||
*
|
*
|
||||||
* @returns {null}
|
* @returns {null}
|
||||||
*/
|
*/
|
||||||
function update (deviceId, logLines) {
|
function update(deviceId, logLines) {
|
||||||
const cs = new pgp.helpers.ColumnSet([
|
const cs = new pgp.helpers.ColumnSet(
|
||||||
'id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'],
|
['id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'],
|
||||||
{table: 'logs'})
|
{ table: 'logs' },
|
||||||
|
)
|
||||||
|
|
||||||
const logs = _.map(log => {
|
const logs = _.map(log => {
|
||||||
const formatted = {
|
const formatted = {
|
||||||
|
|
@ -52,7 +55,7 @@ function update (deviceId, logLines) {
|
||||||
message: log.msg,
|
message: log.msg,
|
||||||
logLevel: _.contains('error', _.lowerCase(log.msg)) ? 'error' : 'info',
|
logLevel: _.contains('error', _.lowerCase(log.msg)) ? 'error' : 'info',
|
||||||
timestamp: log.timestamp,
|
timestamp: log.timestamp,
|
||||||
serial: log.serial || 0
|
serial: log.serial || 0,
|
||||||
}
|
}
|
||||||
return _.mapKeys(_.snakeCase, formatted)
|
return _.mapKeys(_.snakeCase, formatted)
|
||||||
}, logLines)
|
}, logLines)
|
||||||
|
|
@ -61,7 +64,7 @@ function update (deviceId, logLines) {
|
||||||
return db.none(sql)
|
return db.none(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOldLogs () {
|
function clearOldLogs() {
|
||||||
const sqls = `delete from logs
|
const sqls = `delete from logs
|
||||||
where timestamp < now() - interval '3 days';
|
where timestamp < now() - interval '3 days';
|
||||||
delete from server_logs
|
delete from server_logs
|
||||||
|
|
@ -69,7 +72,7 @@ function clearOldLogs () {
|
||||||
return db.multi(sqls)
|
return db.multi(sqls)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
|
function getUnlimitedMachineLogs(deviceId, until = new Date().toISOString()) {
|
||||||
// Note: sql is a little confusing here, since timestamp is used both as a column
|
// Note: sql is a little confusing here, since timestamp is used both as a column
|
||||||
// and a reserved word, but it works.
|
// and a reserved word, but it works.
|
||||||
const sql = `select id, log_level, timestamp, message from logs
|
const sql = `select id, log_level, timestamp, message from logs
|
||||||
|
|
@ -78,14 +81,21 @@ function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
|
||||||
and timestamp > (timestamp $2 - interval '2 days')
|
and timestamp > (timestamp $2 - interval '2 days')
|
||||||
order by timestamp desc, serial desc`
|
order by timestamp desc, serial desc`
|
||||||
|
|
||||||
return Promise.all([db.any(sql, [ deviceId, until ]), getMachineName(deviceId)])
|
return Promise.all([
|
||||||
.then(([logs, machineName]) => ({
|
db.any(sql, [deviceId, until]),
|
||||||
logs: _.map(_.mapKeys(_.camelCase), logs),
|
getMachineName(deviceId),
|
||||||
currentMachine: {deviceId, name: machineName}
|
]).then(([logs, machineName]) => ({
|
||||||
}))
|
logs: _.map(_.mapKeys(_.camelCase), logs),
|
||||||
|
currentMachine: { deviceId, name: machineName },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineLogs (deviceId, until = new Date().toISOString(), limit = null, offset = 0) {
|
function getMachineLogs(
|
||||||
|
deviceId,
|
||||||
|
until = new Date().toISOString(),
|
||||||
|
limit = null,
|
||||||
|
offset = 0,
|
||||||
|
) {
|
||||||
const sql = `select id, log_level, timestamp, message from logs
|
const sql = `select id, log_level, timestamp, message from logs
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
and timestamp <= $2
|
and timestamp <= $2
|
||||||
|
|
@ -93,14 +103,22 @@ function getMachineLogs (deviceId, until = new Date().toISOString(), limit = nul
|
||||||
limit $3
|
limit $3
|
||||||
offset $4`
|
offset $4`
|
||||||
|
|
||||||
return Promise.all([db.any(sql, [ deviceId, until, limit, offset ]), getMachineName(deviceId)])
|
return Promise.all([
|
||||||
.then(([logs, machineName]) => ({
|
db.any(sql, [deviceId, until, limit, offset]),
|
||||||
logs: _.map(_.mapKeys(_.camelCase), logs),
|
getMachineName(deviceId),
|
||||||
currentMachine: {deviceId, name: machineName}
|
]).then(([logs, machineName]) => ({
|
||||||
}))
|
logs: _.map(_.mapKeys(_.camelCase), logs),
|
||||||
|
currentMachine: { deviceId, name: machineName },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
|
function simpleGetMachineLogs(
|
||||||
|
deviceId,
|
||||||
|
from = new Date(0).toISOString(),
|
||||||
|
until = new Date().toISOString(),
|
||||||
|
limit = null,
|
||||||
|
offset = 0,
|
||||||
|
) {
|
||||||
const sql = `select id, log_level, timestamp, message from logs
|
const sql = `select id, log_level, timestamp, message from logs
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
and timestamp >= $2
|
and timestamp >= $2
|
||||||
|
|
@ -109,31 +127,38 @@ function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until
|
||||||
limit $4
|
limit $4
|
||||||
offset $5`
|
offset $5`
|
||||||
|
|
||||||
return db.any(sql, [ deviceId, from, until, limit, offset ])
|
return db
|
||||||
|
.any(sql, [deviceId, from, until, limit, offset])
|
||||||
.then(_.map(_.mapKeys(_.camelCase)))
|
.then(_.map(_.mapKeys(_.camelCase)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function logDateFormat (timezone, logs, fields) {
|
function logDateFormat(timezone, logs, fields) {
|
||||||
return _.map(log => {
|
return _.map(log => {
|
||||||
const values = _.map(
|
const values = _.map(field => {
|
||||||
field =>
|
if (_.isNil(log[field])) return null
|
||||||
{
|
if (!isValid(log[field])) {
|
||||||
if (_.isNil(log[field])) return null
|
logger.warn(
|
||||||
if (!isValid(log[field])) {
|
`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]
|
return log[field]
|
||||||
}
|
}
|
||||||
const date = utcToZonedTime(timezone, log[field])
|
const date = utcToZonedTime(timezone, log[field])
|
||||||
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
|
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
|
||||||
},
|
}, fields)
|
||||||
fields
|
|
||||||
)
|
|
||||||
const fieldsToOverride = _.zipObject(fields, values)
|
const fieldsToOverride = _.zipObject(fields, values)
|
||||||
return {
|
return {
|
||||||
...log,
|
...log,
|
||||||
...fieldsToOverride
|
...fieldsToOverride,
|
||||||
}
|
}
|
||||||
}, logs)
|
}, logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getUnlimitedMachineLogs, getMachineLogs, simpleGetMachineLogs, update, getLastSeen, clearOldLogs, logDateFormat }
|
module.exports = {
|
||||||
|
getUnlimitedMachineLogs,
|
||||||
|
getMachineLogs,
|
||||||
|
simpleGetMachineLogs,
|
||||||
|
update,
|
||||||
|
getLastSeen,
|
||||||
|
clearOldLogs,
|
||||||
|
logDateFormat,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,66 @@
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const pgp = require('pg-promise')()
|
|
||||||
|
|
||||||
function getAvailablePromoCodes () {
|
function getAvailablePromoCodes() {
|
||||||
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
|
const sql = `SELECT * FROM coupons WHERE soft_deleted=false`
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPromoCode (code) {
|
function getPromoCode(code) {
|
||||||
const sql = `SELECT * FROM coupons WHERE code=$1 AND soft_deleted=false`
|
const sql = `SELECT * FROM coupons WHERE code=$1 AND soft_deleted=false`
|
||||||
return db.oneOrNone(sql, [code])
|
return db.oneOrNone(sql, [code])
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPromoCode (code, discount) {
|
function createPromoCode(code, discount) {
|
||||||
const sql = `INSERT INTO coupons (id, code, discount) VALUES ($1, $2, $3) RETURNING *`
|
const sql = `INSERT INTO coupons (id, code, discount) VALUES ($1, $2, $3) RETURNING *`
|
||||||
return db.one(sql, [uuid.v4(), code, discount])
|
return db.one(sql, [uuid.v4(), code, discount])
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePromoCode (id) {
|
function deletePromoCode(id) {
|
||||||
const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1`
|
const sql = `UPDATE coupons SET soft_deleted=true WHERE id=$1`
|
||||||
return db.none(sql, [id])
|
return db.none(sql, [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNumberOfAvailablePromoCodes () {
|
function getNumberOfAvailablePromoCodes() {
|
||||||
const sql = `SELECT COUNT(id) FROM coupons WHERE soft_deleted=false`
|
const sql = `SELECT COUNT(id) FROM coupons WHERE soft_deleted=false`
|
||||||
return db.one(sql).then(res => res.count)
|
return db.one(sql).then(res => res.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableIndividualDiscounts () {
|
function getAvailableIndividualDiscounts() {
|
||||||
const sql = `SELECT * FROM individual_discounts WHERE soft_deleted=false`
|
const sql = `SELECT * FROM individual_discounts WHERE soft_deleted=false`
|
||||||
return db.any(sql).then(res => _.map(it => ({
|
return db.any(sql).then(res =>
|
||||||
id: it.id,
|
_.map(
|
||||||
customerId: it.customer_id,
|
it => ({
|
||||||
discount: it.discount
|
id: it.id,
|
||||||
}), res))
|
customerId: it.customer_id,
|
||||||
|
discount: it.discount,
|
||||||
|
}),
|
||||||
|
res,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomerActiveIndividualDiscount (customerId) {
|
function getCustomerActiveIndividualDiscount(customerId) {
|
||||||
const sql = `SELECT * FROM individual_discounts WHERE customer_id=$1 AND soft_deleted=false LIMIT 1`
|
const sql = `SELECT * FROM individual_discounts WHERE customer_id=$1 AND soft_deleted=false LIMIT 1`
|
||||||
return db.oneOrNone(sql, [customerId]).then(res => {
|
return db.oneOrNone(sql, [customerId]).then(res => {
|
||||||
if (!_.isNil(res)) {
|
if (!_.isNil(res)) {
|
||||||
return {
|
return {
|
||||||
id: res.id,
|
id: res.id,
|
||||||
customerId: res.customer_id,
|
customerId: res.customer_id,
|
||||||
discount: res.discount
|
discount: res.discount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createIndividualDiscount (customerId, discount) {
|
function createIndividualDiscount(customerId, discount) {
|
||||||
const sql = `INSERT INTO individual_discounts (id, customer_id, discount) VALUES ($1, $2, $3)`
|
const sql = `INSERT INTO individual_discounts (id, customer_id, discount) VALUES ($1, $2, $3)`
|
||||||
return db.none(sql, [uuid.v4(), customerId, discount])
|
return db.none(sql, [uuid.v4(), customerId, discount])
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteIndividualDiscount (id) {
|
function deleteIndividualDiscount(id) {
|
||||||
const sql = `UPDATE individual_discounts SET soft_deleted=true WHERE id=$1`
|
const sql = `UPDATE individual_discounts SET soft_deleted=true WHERE id=$1`
|
||||||
return db.none(sql, [id])
|
return db.none(sql, [id])
|
||||||
}
|
}
|
||||||
|
|
@ -70,5 +74,5 @@ module.exports = {
|
||||||
getAvailableIndividualDiscounts,
|
getAvailableIndividualDiscounts,
|
||||||
getCustomerActiveIndividualDiscount,
|
getCustomerActiveIndividualDiscount,
|
||||||
createIndividualDiscount,
|
createIndividualDiscount,
|
||||||
deleteIndividualDiscount
|
deleteIndividualDiscount,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const dbm = require('./postgresql_interface')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
const { GraphQLError } = require('graphql');
|
const { GraphQLError } = require('graphql')
|
||||||
const { loadLatestConfig } = require('./new-settings-loader')
|
const { loadLatestConfig } = require('./new-settings-loader')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ select d.*, COALESCE(emptybills, 0) + COALESCE(regularbills, 0) as cashbox from
|
||||||
group by cit.device_id
|
group by cit.device_id
|
||||||
) as nbills on nbills.device_id = d.device_id`
|
) as nbills on nbills.device_id = d.device_id`
|
||||||
|
|
||||||
function toMachineObject (r) {
|
function toMachineObject(r) {
|
||||||
return {
|
return {
|
||||||
deviceId: r.device_id,
|
deviceId: r.device_id,
|
||||||
cashUnits: {
|
cashUnits: {
|
||||||
|
|
@ -51,48 +51,56 @@ function toMachineObject (r) {
|
||||||
recycler3: r.recycler3,
|
recycler3: r.recycler3,
|
||||||
recycler4: r.recycler4,
|
recycler4: r.recycler4,
|
||||||
recycler5: r.recycler5,
|
recycler5: r.recycler5,
|
||||||
recycler6: r.recycler6
|
recycler6: r.recycler6,
|
||||||
},
|
},
|
||||||
numberOfCassettes: r.number_of_cassettes,
|
numberOfCassettes: r.number_of_cassettes,
|
||||||
numberOfRecyclers: r.number_of_recyclers,
|
numberOfRecyclers: r.number_of_recyclers,
|
||||||
version: r.version,
|
version: r.version,
|
||||||
model: r.model,
|
model: r.model,
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
timestamp: r.diagnostics_timestamp? new Date(r.diagnostics_timestamp) : null,
|
timestamp: r.diagnostics_timestamp
|
||||||
scanTimestamp: r.diagnostics_scan_timestamp? new Date(r.diagnostics_scan_timestamp) : null,
|
? new Date(r.diagnostics_timestamp)
|
||||||
frontTimestamp: r.diagnostics_front_timestamp? new Date(r.diagnostics_front_timestamp) : null
|
: 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),
|
pairedAt: new Date(r.created),
|
||||||
lastPing: new Date(r.last_online),
|
lastPing: new Date(r.last_online),
|
||||||
name: r.name,
|
name: r.name,
|
||||||
paired: r.paired
|
paired: r.paired,
|
||||||
// TODO: we shall start using this JSON field at some point
|
// TODO: we shall start using this JSON field at some point
|
||||||
// location: r.location,
|
// location: r.location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineIds () {
|
function getMachineIds() {
|
||||||
const sql = 'select device_id from devices'
|
const sql = 'select device_id from devices'
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachines () {
|
function getMachines() {
|
||||||
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} where display=TRUE ORDER BY created`
|
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} where display=TRUE ORDER BY created`
|
||||||
return db.any(sql)
|
return db.any(sql).then(rr => rr.map(toMachineObject))
|
||||||
.then(rr => rr.map(toMachineObject))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnpairedMachines () {
|
function getUnpairedMachines() {
|
||||||
return db.any('SELECT * FROM unpaired_devices')
|
return db
|
||||||
.then(_.map(r =>
|
.any('SELECT * FROM unpaired_devices')
|
||||||
_.flow(
|
.then(
|
||||||
_.set('deviceId', _.get('device_id', r)),
|
_.map(r =>
|
||||||
_.unset('device_id')
|
_.flow(
|
||||||
)(r)
|
_.set('deviceId', _.get('device_id', r)),
|
||||||
))
|
_.unset('device_id'),
|
||||||
|
)(r),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig (defaultConfig) {
|
function getConfig(defaultConfig) {
|
||||||
return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig()
|
return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +112,7 @@ const getStatus = (ping, stuck) => {
|
||||||
return fullyFunctionalStatus
|
return fullyFunctionalStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
function addName (pings, events, config) {
|
function addName(pings, events, config) {
|
||||||
return machine => {
|
return machine => {
|
||||||
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
|
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
|
||||||
|
|
||||||
|
|
@ -113,22 +121,38 @@ function addName (pings, events, config) {
|
||||||
const statuses = [
|
const statuses = [
|
||||||
getStatus(
|
getStatus(
|
||||||
_.first(pings[machine.deviceId]),
|
_.first(pings[machine.deviceId]),
|
||||||
_.first(checkStuckScreen(events, machine))
|
_.first(checkStuckScreen(events, machine)),
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
return _.assign(machine, { cashOut, statuses })
|
return _.assign(machine, { cashOut, statuses })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineNames (config) {
|
function getMachineNames(config) {
|
||||||
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
|
return Promise.all([
|
||||||
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
|
getMachines(),
|
||||||
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
|
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]) => {
|
.then(([rawMachines, pings, events, config, heartbeat, performance]) => {
|
||||||
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
|
const mergeByDeviceId = (x, y) =>
|
||||||
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
|
_.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
|
||||||
|
const machines = mergeByDeviceId(
|
||||||
|
mergeByDeviceId(rawMachines, heartbeat),
|
||||||
|
performance,
|
||||||
|
)
|
||||||
|
|
||||||
return machines.map(addName(pings, events, config))
|
return machines.map(addName(pings, events, config))
|
||||||
})
|
})
|
||||||
|
|
@ -144,67 +168,133 @@ function getMachineNames (config) {
|
||||||
* @param {string} machineId machine id
|
* @param {string} machineId machine id
|
||||||
* @returns {string} machine name
|
* @returns {string} machine name
|
||||||
*/
|
*/
|
||||||
function getMachineName (machineId) {
|
function getMachineName(machineId) {
|
||||||
const sql = 'SELECT name FROM devices WHERE device_id=$1'
|
const sql = 'SELECT name FROM devices WHERE device_id=$1'
|
||||||
return db.oneOrNone(sql, [machineId])
|
return db.oneOrNone(sql, [machineId]).then(it => it?.name)
|
||||||
.then(it => it?.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachine (machineId, config) {
|
function getMachine(machineId, config) {
|
||||||
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
|
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
|
||||||
|
|
||||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
|
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)
|
else return toMachineObject(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.all([queryMachine, dbm.machineEvents(), config, getNetworkHeartbeatByDevice(machineId), getNetworkPerformanceByDevice(machineId)])
|
return Promise.all([
|
||||||
.then(([machine, events, config, heartbeat, performance]) => {
|
queryMachine,
|
||||||
const pings = checkPings([machine])
|
dbm.machineEvents(),
|
||||||
const mergedMachine = {
|
config,
|
||||||
...machine,
|
getNetworkHeartbeatByDevice(machineId),
|
||||||
responseTime: _.get('responseTime', heartbeat),
|
getNetworkPerformanceByDevice(machineId),
|
||||||
packetLoss: _.get('packetLoss', heartbeat),
|
]).then(([machine, events, config, heartbeat, performance]) => {
|
||||||
downloadSpeed: _.get('downloadSpeed', performance),
|
const pings = checkPings([machine])
|
||||||
}
|
const mergedMachine = {
|
||||||
|
...machine,
|
||||||
|
responseTime: _.get('responseTime', heartbeat),
|
||||||
|
packetLoss: _.get('packetLoss', heartbeat),
|
||||||
|
downloadSpeed: _.get('downloadSpeed', performance),
|
||||||
|
}
|
||||||
|
|
||||||
return addName(pings, events, config)(mergedMachine)
|
return addName(pings, events, config)(mergedMachine)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameMachine (rec) {
|
function renameMachine(rec) {
|
||||||
const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
|
const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
|
||||||
return db.none(sql, [rec.newName, rec.deviceId])
|
return db.none(sql, [rec.newName, rec.deviceId])
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCashOutBills (rec) {
|
function resetCashOutBills(rec) {
|
||||||
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
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;`
|
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) {
|
function setCassetteBills(rec) {
|
||||||
const { cashbox, cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits
|
const {
|
||||||
return getMachine(rec.deviceId)
|
cashbox,
|
||||||
.then(machine => {
|
cassette1,
|
||||||
const oldCashboxCount = machine?.cashUnits?.cashbox
|
cassette2,
|
||||||
if (_.isNil(oldCashboxCount) || cashbox.toString() === oldCashboxCount.toString()) {
|
cassette3,
|
||||||
const sql = `
|
cassette4,
|
||||||
|
recycler1,
|
||||||
|
recycler2,
|
||||||
|
recycler3,
|
||||||
|
recycler4,
|
||||||
|
recycler5,
|
||||||
|
recycler6,
|
||||||
|
} = rec.cashUnits
|
||||||
|
return getMachine(rec.deviceId).then(machine => {
|
||||||
|
const oldCashboxCount = machine?.cashUnits?.cashbox
|
||||||
|
if (
|
||||||
|
_.isNil(oldCashboxCount) ||
|
||||||
|
cashbox.toString() === oldCashboxCount.toString()
|
||||||
|
) {
|
||||||
|
const sql = `
|
||||||
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
|
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
|
||||||
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
|
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
|
||||||
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6)
|
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6)
|
||||||
WHERE device_id=$11`
|
WHERE device_id=$11`
|
||||||
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId])
|
return db.none(sql, [
|
||||||
}
|
cassette1,
|
||||||
|
cassette2,
|
||||||
|
cassette3,
|
||||||
|
cassette4,
|
||||||
|
recycler1,
|
||||||
|
recycler2,
|
||||||
|
recycler3,
|
||||||
|
recycler4,
|
||||||
|
recycler5,
|
||||||
|
recycler6,
|
||||||
|
rec.deviceId,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue: oldCashboxCount })
|
return batching.updateMachineWithBatch({
|
||||||
|
...rec,
|
||||||
|
oldCashboxValue: oldCashboxCount,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
function emptyMachineUnits({ deviceId, newUnits, fiatCode }) {
|
||||||
return loadLatestConfig()
|
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]) => {
|
.then(([machine, cashoutSettings]) => {
|
||||||
const movedBills = _.reduce(
|
const movedBills = _.reduce(
|
||||||
(acc, value) => ({
|
(acc, value) => ({
|
||||||
|
|
@ -212,34 +302,42 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
||||||
[value]: {
|
[value]: {
|
||||||
operationName: `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`,
|
operationName: `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`,
|
||||||
delta: newUnits[value] - machine.cashUnits[value],
|
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 => ({
|
const operationsToCreate = _.map(it => ({
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
operation_type: it
|
operation_type: it,
|
||||||
}))(operationNames)
|
}))(operationNames)
|
||||||
|
|
||||||
const billArr = _.reduce(
|
const billArr = _.reduce(
|
||||||
(acc, value) => {
|
(acc, value) => {
|
||||||
const unit = movedBills[value]
|
const unit = movedBills[value]
|
||||||
return _.concat(acc, _.times(() => ({
|
return _.concat(
|
||||||
id: uuid.v4(),
|
acc,
|
||||||
fiat: unit.denomination,
|
_.times(
|
||||||
fiat_code: fiatCode,
|
() => ({
|
||||||
device_id: deviceId
|
id: uuid.v4(),
|
||||||
// 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
|
fiat: unit.denomination,
|
||||||
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id
|
fiat_code: fiatCode,
|
||||||
}), Math.abs(unit.delta)))
|
device_id: deviceId,
|
||||||
|
// TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event
|
||||||
|
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id
|
||||||
|
}),
|
||||||
|
Math.abs(unit.delta),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
_.keys(_.omit(['cashbox'], movedBills))
|
_.keys(_.omit(['cashbox'], movedBills)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// This occurs when an empty unit is called when the units are already empty, hence, no bills moved around
|
// This occurs when an empty unit is called when the units are already empty, hence, no bills moved around
|
||||||
|
|
@ -249,59 +347,76 @@ function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
||||||
|
|
||||||
return db.tx(t => {
|
return db.tx(t => {
|
||||||
const q1Cols = ['id', 'device_id', 'operation_type']
|
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 q2Cols = ['id', 'fiat', 'fiat_code', 'device_id']
|
||||||
const q2 = t.none(pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'))
|
const q2 = t.none(
|
||||||
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`, [
|
pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'),
|
||||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
)
|
||||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
const q3 = t.none(
|
||||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
`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.cassette4, newUnits.cassette4),
|
[
|
||||||
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
|
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||||
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
|
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||||
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
|
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
|
||||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
|
||||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
|
||||||
deviceId
|
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
|
||||||
])
|
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||||
|
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||||
|
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||||
|
deviceId,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
return t.batch([q1, q2, q3])
|
return t.batch([q1, q2, q3])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refillMachineUnits ({ deviceId, newUnits }) {
|
function refillMachineUnits({ deviceId, newUnits }) {
|
||||||
return getMachine(deviceId)
|
return getMachine(deviceId).then(machine => {
|
||||||
.then(machine => {
|
const movedBills = _.reduce(
|
||||||
const movedBills = _.reduce(
|
(acc, value) => ({
|
||||||
(acc, value) => ({
|
...acc,
|
||||||
...acc,
|
[value]: {
|
||||||
[value]: {
|
operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`,
|
||||||
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 operationsToCreate = _.map(it => ({
|
||||||
|
id: uuid.v4(),
|
||||||
|
device_id: deviceId,
|
||||||
|
operation_type: it,
|
||||||
|
}))(operationNames)
|
||||||
|
|
||||||
|
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
|
||||||
|
if (_.isEmpty(operationsToCreate)) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.tx(t => {
|
||||||
|
const q1Cols = ['id', 'device_id', 'operation_type']
|
||||||
|
const q1 = t.none(
|
||||||
|
pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'),
|
||||||
)
|
)
|
||||||
|
const q2 = t.none(
|
||||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills)))
|
`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 operationsToCreate = _.map(it => ({
|
[
|
||||||
id: uuid.v4(),
|
|
||||||
device_id: deviceId,
|
|
||||||
operation_type: it
|
|
||||||
}))(operationNames)
|
|
||||||
|
|
||||||
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
|
|
||||||
if (_.isEmpty(operationsToCreate)) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.tx(t => {
|
|
||||||
const q1Cols = ['id', 'device_id', 'operation_type']
|
|
||||||
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
|
|
||||||
const q2 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [
|
|
||||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||||
|
|
@ -312,64 +427,70 @@ function refillMachineUnits ({ deviceId, newUnits }) {
|
||||||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||||
deviceId
|
deviceId,
|
||||||
])
|
],
|
||||||
|
)
|
||||||
|
|
||||||
return t.batch([q1, q2])
|
return t.batch([q1, q2])
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function unpair (rec) {
|
function unpair(rec) {
|
||||||
return pairing.unpair(rec.deviceId)
|
return pairing.unpair(rec.deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function reboot (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',
|
action: 'reboot',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
}
|
}),
|
||||||
)])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function shutdown (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',
|
action: 'shutdown',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
}
|
}),
|
||||||
)])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartServices (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',
|
action: 'restartServices',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
}
|
}),
|
||||||
)])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyUnit (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',
|
action: 'emptyUnit',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
}
|
}),
|
||||||
)])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function refillUnit (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',
|
action: 'refillUnit',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
}
|
}),
|
||||||
)])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function diagnostics (rec) {
|
function diagnostics(rec) {
|
||||||
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${rec.deviceId}/`
|
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${rec.deviceId}/`
|
||||||
const sql = `UPDATE devices
|
const sql = `UPDATE devices
|
||||||
SET diagnostics_timestamp = NULL,
|
SET diagnostics_timestamp = NULL,
|
||||||
|
|
@ -391,48 +512,65 @@ function diagnostics (rec) {
|
||||||
|
|
||||||
return Promise.all(removeFiles)
|
return Promise.all(removeFiles)
|
||||||
.then(() => db.none(sql, [rec.deviceId]))
|
.then(() => db.none(sql, [rec.deviceId]))
|
||||||
.then(() => db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
.then(() =>
|
||||||
{
|
db.none('NOTIFY $1:name, $2', [
|
||||||
action: 'diagnostics',
|
'machineAction',
|
||||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
JSON.stringify({
|
||||||
}
|
action: 'diagnostics',
|
||||||
)]))
|
value: _.pick(['deviceId', 'operatorId', 'action'], rec),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMachine (rec, operatorId) {
|
function setMachine(rec, operatorId) {
|
||||||
rec.operatorId = operatorId
|
rec.operatorId = operatorId
|
||||||
switch (rec.action) {
|
switch (rec.action) {
|
||||||
case 'rename': return renameMachine(rec)
|
case 'rename':
|
||||||
case 'resetCashOutBills': return resetCashOutBills(rec)
|
return renameMachine(rec)
|
||||||
case 'setCassetteBills': return setCassetteBills(rec)
|
case 'resetCashOutBills':
|
||||||
case 'unpair': return unpair(rec)
|
return resetCashOutBills(rec)
|
||||||
case 'reboot': return reboot(rec)
|
case 'setCassetteBills':
|
||||||
case 'shutdown': return shutdown(rec)
|
return setCassetteBills(rec)
|
||||||
case 'restartServices': return restartServices(rec)
|
case 'unpair':
|
||||||
case 'emptyUnit': return emptyUnit(rec)
|
return unpair(rec)
|
||||||
case 'refillUnit': return refillUnit(rec)
|
case 'reboot':
|
||||||
case 'diagnostics': return diagnostics(rec)
|
return reboot(rec)
|
||||||
default: throw new Error('No such action: ' + rec.action)
|
case 'shutdown':
|
||||||
|
return shutdown(rec)
|
||||||
|
case 'restartServices':
|
||||||
|
return restartServices(rec)
|
||||||
|
case 'emptyUnit':
|
||||||
|
return emptyUnit(rec)
|
||||||
|
case 'refillUnit':
|
||||||
|
return refillUnit(rec)
|
||||||
|
case 'diagnostics':
|
||||||
|
return diagnostics(rec)
|
||||||
|
default:
|
||||||
|
throw new Error('No such action: ' + rec.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNetworkPerformance (deviceId, data) {
|
function updateNetworkPerformance(deviceId, data) {
|
||||||
if (_.isEmpty(data)) return Promise.resolve(true)
|
if (_.isEmpty(data)) return Promise.resolve(true)
|
||||||
const downloadSpeed = _.head(data)
|
const downloadSpeed = _.head(data)
|
||||||
const dbData = {
|
const dbData = {
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
download_speed: downloadSpeed.speed,
|
download_speed: downloadSpeed.speed,
|
||||||
created: new Date()
|
created: new Date(),
|
||||||
}
|
}
|
||||||
const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
|
const cs = new pgp.helpers.ColumnSet(
|
||||||
{ table: 'machine_network_performance' })
|
['device_id', 'download_speed', 'created'],
|
||||||
const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
|
{ table: 'machine_network_performance' },
|
||||||
|
)
|
||||||
|
const onConflict =
|
||||||
|
' ON CONFLICT (device_id) DO UPDATE SET ' +
|
||||||
cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
|
cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
|
||||||
const upsert = pgp.helpers.insert(dbData, cs) + onConflict
|
const upsert = pgp.helpers.insert(dbData, cs) + onConflict
|
||||||
return db.none(upsert)
|
return db.none(upsert)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNetworkHeartbeat (deviceId, data) {
|
function updateNetworkHeartbeat(deviceId, data) {
|
||||||
if (_.isEmpty(data)) return Promise.resolve(true)
|
if (_.isEmpty(data)) return Promise.resolve(true)
|
||||||
const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
|
const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
|
||||||
const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
|
const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
|
||||||
|
|
@ -440,41 +578,47 @@ function updateNetworkHeartbeat (deviceId, data) {
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
average_response_time: avgResponseTime,
|
average_response_time: avgResponseTime,
|
||||||
average_packet_loss: avgPacketLoss
|
average_packet_loss: avgPacketLoss,
|
||||||
}
|
}
|
||||||
const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
|
const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
|
||||||
return db.none(sql)
|
return db.none(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNetworkPerformance () {
|
function getNetworkPerformance() {
|
||||||
const sql = `SELECT device_id, download_speed FROM machine_network_performance`
|
const sql = `SELECT device_id, download_speed FROM machine_network_performance`
|
||||||
return db.manyOrNone(sql)
|
return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res))
|
||||||
.then(res => _.map(_.mapKeys(_.camelCase))(res))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNetworkHeartbeat () {
|
function getNetworkHeartbeat() {
|
||||||
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
|
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
|
||||||
FROM machine_network_heartbeat
|
FROM machine_network_heartbeat
|
||||||
GROUP BY device_id`
|
GROUP BY device_id`
|
||||||
return db.manyOrNone(sql)
|
return db.manyOrNone(sql).then(res => _.map(_.mapKeys(_.camelCase))(res))
|
||||||
.then(res => _.map(_.mapKeys(_.camelCase))(res))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNetworkPerformanceByDevice (deviceId) {
|
function getNetworkPerformanceByDevice(deviceId) {
|
||||||
const sql = `SELECT device_id, download_speed FROM machine_network_performance WHERE device_id = $1`
|
const sql = `SELECT device_id, download_speed FROM machine_network_performance WHERE device_id = $1`
|
||||||
return db.manyOrNone(sql, [deviceId])
|
return db.manyOrNone(sql, [deviceId]).then(res =>
|
||||||
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
|
_.mapKeys(
|
||||||
|
_.camelCase,
|
||||||
|
_.find(it => it.device_id === deviceId, res),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNetworkHeartbeatByDevice (deviceId) {
|
function getNetworkHeartbeatByDevice(deviceId) {
|
||||||
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
|
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
|
FROM machine_network_heartbeat WHERE device_id = $1
|
||||||
GROUP BY device_id`
|
GROUP BY device_id`
|
||||||
return db.manyOrNone(sql, [deviceId])
|
return db.manyOrNone(sql, [deviceId]).then(res =>
|
||||||
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
|
_.mapKeys(
|
||||||
|
_.camelCase,
|
||||||
|
_.find(it => it.device_id === deviceId, res),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDiagnostics (deviceId, images) {
|
function updateDiagnostics(deviceId, images) {
|
||||||
const sql = `UPDATE devices
|
const sql = `UPDATE devices
|
||||||
SET diagnostics_timestamp = NOW(),
|
SET diagnostics_timestamp = NOW(),
|
||||||
diagnostics_scan_updated_at = CASE WHEN $2 THEN NOW() ELSE diagnostics_scan_updated_at END,
|
diagnostics_scan_updated_at = CASE WHEN $2 THEN NOW() ELSE diagnostics_scan_updated_at END,
|
||||||
|
|
@ -484,19 +628,25 @@ function updateDiagnostics (deviceId, images) {
|
||||||
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/`
|
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/`
|
||||||
const { scan, front } = images
|
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]))
|
.then(() => db.none(sql, [deviceId, !!scan, !!front]))
|
||||||
.catch(err => logger.error('while running machine diagnostics: ', err))
|
.catch(err => logger.error('while running machine diagnostics: ', err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFailedQRScans = (deviceId, frames) => {
|
const updateFailedQRScans = (deviceId, frames) => {
|
||||||
const timestamp = (new Date()).toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
const directory = `${OPERATOR_DATA_DIR}/failedQRScans/${deviceId}/`
|
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))
|
return updatePhotos(directory, _.zip(filenames, frames))
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPhoto (name, data, dir) {
|
function createPhoto(name, data, dir) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.error(`Diagnostics error: No data to save for ${name} photo`)
|
logger.error(`Diagnostics error: No data to save for ${name} photo`)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|
@ -507,12 +657,12 @@ function createPhoto (name, data, dir) {
|
||||||
return fsPromises.writeFile(filename, decodedImageData)
|
return fsPromises.writeFile(filename, decodedImageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePhotos (dir, photoPairs) {
|
function updatePhotos(dir, photoPairs) {
|
||||||
const dirname = path.join(dir)
|
const dirname = path.join(dir)
|
||||||
_.attempt(() => makeDir.sync(dirname))
|
_.attempt(() => makeDir.sync(dirname))
|
||||||
return Promise.all(photoPairs.map(
|
return Promise.all(
|
||||||
([filename, data]) => createPhoto(filename, data, dirname)
|
photoPairs.map(([filename, data]) => createPhoto(filename, data, dirname)),
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -530,5 +680,5 @@ module.exports = {
|
||||||
emptyMachineUnits,
|
emptyMachineUnits,
|
||||||
refillMachineUnits,
|
refillMachineUnits,
|
||||||
updateDiagnostics,
|
updateDiagnostics,
|
||||||
updateFailedQRScans
|
updateFailedQRScans,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ const pairing = require('../pairing')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
const authorize = function (req, res, next) {
|
const authorize = function (req, res, next) {
|
||||||
return pairing.isPaired(req.deviceId)
|
return pairing
|
||||||
|
.isPaired(req.deviceId)
|
||||||
.then(deviceName => {
|
.then(deviceName => {
|
||||||
if (deviceName) {
|
if (deviceName) {
|
||||||
req.deviceName = deviceName
|
req.deviceName = deviceName
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
const pairing = require('../pairing')
|
const pairing = require('../pairing')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
function ca (req, res) {
|
function ca(req, res) {
|
||||||
const token = req.query.token
|
const token = req.query.token
|
||||||
|
|
||||||
return pairing.authorizeCaDownload(token)
|
return pairing
|
||||||
|
.authorizeCaDownload(token)
|
||||||
.then(ca => res.json({ ca }))
|
.then(ca => res.json({ ca }))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error(error.message)
|
logger.error(error.message)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
function errorHandler (err, req, res, next) {
|
function errorHandler(err, req, res) {
|
||||||
const statusCode = err.name === 'HTTPError'
|
const statusCode = err.name === 'HTTPError' ? err.code || 500 : 500
|
||||||
? err.code || 500
|
|
||||||
: 500
|
|
||||||
|
|
||||||
const json = { error: err.message }
|
const json = { error: err.message }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,23 @@ const CLOCK_SKEW = 60 * 1000
|
||||||
const REQUEST_TTL = 3 * 60 * 1000
|
const REQUEST_TTL = 3 * 60 * 1000
|
||||||
const THROTTLE_CLOCK_SKEW = 60 * 1000
|
const THROTTLE_CLOCK_SKEW = 60 * 1000
|
||||||
|
|
||||||
function filterOldRequests (req, res, next) {
|
function filterOldRequests(req, res, next) {
|
||||||
const deviceTime = req.deviceTime
|
const deviceTime = req.deviceTime
|
||||||
const deviceId = req.deviceId
|
const deviceId = req.deviceId
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const delta = timestamp - Date.parse(deviceTime)
|
const delta = timestamp - Date.parse(deviceTime)
|
||||||
|
|
||||||
const shouldTrigger = !state.canLogClockSkewMap[deviceId] ||
|
const shouldTrigger =
|
||||||
|
!state.canLogClockSkewMap[deviceId] ||
|
||||||
timestamp - state.canLogClockSkewMap[deviceId] >= THROTTLE_CLOCK_SKEW
|
timestamp - state.canLogClockSkewMap[deviceId] >= THROTTLE_CLOCK_SKEW
|
||||||
|
|
||||||
if (delta > CLOCK_SKEW && shouldTrigger) {
|
if (delta > CLOCK_SKEW && shouldTrigger) {
|
||||||
state.canLogClockSkewMap[deviceId] = timestamp
|
state.canLogClockSkewMap[deviceId] = timestamp
|
||||||
logger.error('Clock skew with lamassu-machine[%s] too high [%ss], adjust lamassu-machine clock',
|
logger.error(
|
||||||
req.deviceName, (delta / 1000).toFixed(2))
|
'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' })
|
if (delta > REQUEST_TTL) return res.status(408).json({ error: 'stale' })
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const { getOperatorId } = require('../operator')
|
const { getOperatorId } = require('../operator')
|
||||||
|
|
||||||
function findOperatorId (req, res, next) {
|
function findOperatorId(req, res, next) {
|
||||||
return getOperatorId('middleware')
|
return getOperatorId('middleware')
|
||||||
.then(operatorId => {
|
.then(operatorId => {
|
||||||
res.locals.operatorId = operatorId
|
res.locals.operatorId = operatorId
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
|
||||||
function sha256 (buf) {
|
function sha256(buf) {
|
||||||
if (!buf) return null
|
if (!buf) return null
|
||||||
const hash = crypto.createHash('sha256')
|
const hash = crypto.createHash('sha256')
|
||||||
|
|
||||||
|
|
@ -9,10 +9,13 @@ function sha256 (buf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const populateDeviceId = function (req, res, next) {
|
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
|
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.deviceId = deviceId
|
||||||
req.deviceTime = req.get('date')
|
req.deviceTime = req.get('date')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,74 @@ const state = require('./state')
|
||||||
const newSettingsLoader = require('../new-settings-loader')
|
const newSettingsLoader = require('../new-settings-loader')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
db.connect({ direct: true }).then(sco => {
|
db.connect({ direct: true })
|
||||||
sco.client.on('notification', data => {
|
.then(sco => {
|
||||||
const parsedData = JSON.parse(data.payload)
|
sco.client.on('notification', data => {
|
||||||
return reload(parsedData.operatorId)
|
const parsedData = JSON.parse(data.payload)
|
||||||
|
return reload(parsedData.operatorId)
|
||||||
|
})
|
||||||
|
return sco.none('LISTEN $1:name', 'reload')
|
||||||
})
|
})
|
||||||
return sco.none('LISTEN $1:name', 'reload')
|
.catch(console.error)
|
||||||
}).catch(console.error)
|
|
||||||
|
|
||||||
db.connect({ direct: true }).then(sco => {
|
db.connect({ direct: true })
|
||||||
sco.client.on('notification', data => {
|
.then(sco => {
|
||||||
const parsedData = JSON.parse(data.payload)
|
sco.client.on('notification', data => {
|
||||||
return machineAction(parsedData.action, parsedData.value)
|
const parsedData = JSON.parse(data.payload)
|
||||||
|
return machineAction(parsedData.action, parsedData.value)
|
||||||
|
})
|
||||||
|
return sco.none('LISTEN $1:name', 'machineAction')
|
||||||
})
|
})
|
||||||
return sco.none('LISTEN $1:name', 'machineAction')
|
.catch(console.error)
|
||||||
}).catch(console.error)
|
|
||||||
|
|
||||||
function machineAction (type, value) {
|
function machineAction(type, value) {
|
||||||
const deviceId = value.deviceId
|
const deviceId = value.deviceId
|
||||||
const operatorId = value.operatorId
|
const operatorId = value.operatorId
|
||||||
const pid = state.pids?.[operatorId]?.[deviceId]?.pid
|
const pid = state.pids?.[operatorId]?.[deviceId]?.pid
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'reboot':
|
case 'reboot':
|
||||||
logger.debug(`Rebooting machine '${deviceId}' from operator ${operatorId}`)
|
logger.debug(
|
||||||
|
`Rebooting machine '${deviceId}' from operator ${operatorId}`,
|
||||||
|
)
|
||||||
state.reboots[operatorId] = { [deviceId]: pid }
|
state.reboots[operatorId] = { [deviceId]: pid }
|
||||||
break
|
break
|
||||||
case 'shutdown':
|
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 }
|
state.shutdowns[operatorId] = { [deviceId]: pid }
|
||||||
break
|
break
|
||||||
case 'restartServices':
|
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 }
|
state.restartServicesMap[operatorId] = { [deviceId]: pid }
|
||||||
break
|
break
|
||||||
case 'emptyUnit':
|
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 }
|
state.emptyUnit[operatorId] = { [deviceId]: pid }
|
||||||
break
|
break
|
||||||
case 'refillUnit':
|
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 }
|
state.refillUnit[operatorId] = { [deviceId]: pid }
|
||||||
break
|
break
|
||||||
case 'diagnostics':
|
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 }
|
state.diagnostics[operatorId] = { [deviceId]: pid }
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload (operatorId) {
|
function reload(operatorId) {
|
||||||
state.needsSettingsReload[operatorId] = true
|
state.needsSettingsReload[operatorId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,11 +90,14 @@ const populateSettings = function (req, res, next) {
|
||||||
// 4. There's no cached config, cache and send the latest config
|
// 4. There's no cached config, cache and send the latest config
|
||||||
|
|
||||||
if (versionId) {
|
if (versionId) {
|
||||||
const cachedVersionedSettings = settingsCache.get(`${operatorId}-v${versionId}`)
|
const cachedVersionedSettings = settingsCache.get(
|
||||||
|
`${operatorId}-v${versionId}`,
|
||||||
|
)
|
||||||
|
|
||||||
if (!cachedVersionedSettings) {
|
if (!cachedVersionedSettings) {
|
||||||
logger.debug('Fetching a specific config version cached value')
|
logger.debug('Fetching a specific config version cached value')
|
||||||
return newSettingsLoader.load(versionId)
|
return newSettingsLoader
|
||||||
|
.load(versionId)
|
||||||
.then(settings => {
|
.then(settings => {
|
||||||
settingsCache.set(`${operatorId}-v${versionId}`, settings)
|
settingsCache.set(`${operatorId}-v${versionId}`, settings)
|
||||||
req.settings = settings
|
req.settings = settings
|
||||||
|
|
@ -94,16 +114,22 @@ const populateSettings = function (req, res, next) {
|
||||||
const operatorSettings = settingsCache.get(`${operatorId}-latest`)
|
const operatorSettings = settingsCache.get(`${operatorId}-latest`)
|
||||||
|
|
||||||
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
|
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
|
||||||
!!needsSettingsReload[operatorId]
|
needsSettingsReload[operatorId]
|
||||||
? logger.debug('Fetching and caching a new latest config value, as a reload was requested')
|
? logger.debug(
|
||||||
: logger.debug('Fetching the latest config version because there\'s no cached value')
|
'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 => {
|
.then(settings => {
|
||||||
const versionId = settings.version
|
const versionId = settings.version
|
||||||
settingsCache.set(`${operatorId}-latest`, settings)
|
settingsCache.set(`${operatorId}-latest`, settings)
|
||||||
settingsCache.set(`${operatorId}-v${versionId}`, settings)
|
settingsCache.set(`${operatorId}-v${versionId}`, settings)
|
||||||
if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId]
|
if (needsSettingsReload[operatorId])
|
||||||
|
delete needsSettingsReload[operatorId]
|
||||||
req.settings = settings
|
req.settings = settings
|
||||||
})
|
})
|
||||||
.then(() => next())
|
.then(() => next())
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,20 @@ const rejectIncompatibleMachines = function (req, res, next) {
|
||||||
const machineMajor = semver.major(machineVersion)
|
const machineMajor = semver.major(machineVersion)
|
||||||
|
|
||||||
if (serverMajor - machineMajor > 1) {
|
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({
|
return res.status(400).json({
|
||||||
error: 'Machine version too old'
|
error: 'Machine version too old',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverMajor < machineMajor) {
|
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({
|
return res.status(400).json({
|
||||||
error: 'Machine version too new'
|
error: 'Machine version too new',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module.exports = (function () {
|
||||||
needsSettingsReload: {},
|
needsSettingsReload: {},
|
||||||
settingsCache: new NodeCache({
|
settingsCache: new NodeCache({
|
||||||
stdTTL: SETTINGS_CACHE_REFRESH,
|
stdTTL: SETTINGS_CACHE_REFRESH,
|
||||||
checkperiod: SETTINGS_CACHE_REFRESH // Clear cache every hour
|
checkperiod: SETTINGS_CACHE_REFRESH, // Clear cache every hour
|
||||||
}),
|
}),
|
||||||
canLogClockSkewMap: {},
|
canLogClockSkewMap: {},
|
||||||
canGetLastSeenMap: {},
|
canGetLastSeenMap: {},
|
||||||
|
|
@ -18,6 +18,6 @@ module.exports = (function () {
|
||||||
emptyUnit: {},
|
emptyUnit: {},
|
||||||
refillUnit: {},
|
refillUnit: {},
|
||||||
diagnostics: {},
|
diagnostics: {},
|
||||||
mnemonic: null
|
mnemonic: null,
|
||||||
}
|
}
|
||||||
}())
|
})()
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ const migrateDir = path.resolve(__dirname, '..', 'migrations')
|
||||||
const migrateOpts = {
|
const migrateOpts = {
|
||||||
migrationsDirectory: migrateDir,
|
migrationsDirectory: migrateDir,
|
||||||
stateStore: new DbMigrateStore(),
|
stateStore: new DbMigrateStore(),
|
||||||
filterFunction: it => it.match(/^\d+.*\.js$/)
|
filterFunction: it => it.match(/^\d+.*\.js$/),
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { run }
|
module.exports = { run }
|
||||||
function run () {
|
function run() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
migrate.load(migrateOpts, (err, set) => {
|
migrate.load(migrateOpts, (err, set) => {
|
||||||
if (err) return reject(err)
|
if (err) return reject(err)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const bip39 = require('bip39')
|
const bip39 = require('bip39')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
|
|
||||||
function fromSeed (seed) {
|
function fromSeed(seed) {
|
||||||
const words = bip39.entropyToMnemonic(seed).split(' ')
|
const words = bip39.entropyToMnemonic(seed).split(' ')
|
||||||
|
|
||||||
let mnemonic = ''
|
let mnemonic = ''
|
||||||
|
|
@ -11,7 +11,7 @@ function fromSeed (seed) {
|
||||||
return mnemonic
|
return mnemonic
|
||||||
}
|
}
|
||||||
|
|
||||||
function toEntropyBuffer (mnemonic) {
|
function toEntropyBuffer(mnemonic) {
|
||||||
const hex = bip39.mnemonicToEntropy(mnemonic.split('\n').join(' ').trim())
|
const hex = bip39.mnemonicToEntropy(mnemonic.split('\n').join(' ').trim())
|
||||||
return Buffer.from(hex.trim(), 'hex')
|
return Buffer.from(hex.trim(), 'hex')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ const nocache = require('nocache')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const { ApolloServer } = require('@apollo/server')
|
const { ApolloServer } = require('@apollo/server')
|
||||||
const { expressMiddleware } = require('@apollo/server/express4')
|
const { expressMiddleware } = require('@apollo/server/express4')
|
||||||
const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled')
|
const {
|
||||||
const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default')
|
ApolloServerPluginLandingPageDisabled,
|
||||||
|
} = require('@apollo/server/plugin/disabled')
|
||||||
|
const {
|
||||||
|
ApolloServerPluginLandingPageLocalDefault,
|
||||||
|
} = require('@apollo/server/plugin/landingPage/default')
|
||||||
|
|
||||||
const { mergeResolvers } = require('@graphql-tools/merge')
|
const { mergeResolvers } = require('@graphql-tools/merge')
|
||||||
const { makeExecutableSchema } = require('@graphql-tools/schema')
|
const { makeExecutableSchema } = require('@graphql-tools/schema')
|
||||||
|
|
@ -23,7 +27,11 @@ const { authDirectiveTransformer } = require('./graphql/directives')
|
||||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
const { typeDefs, resolvers } = require('./graphql/schema')
|
||||||
const findOperatorId = require('../middlewares/operatorId')
|
const findOperatorId = require('../middlewares/operatorId')
|
||||||
const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants')
|
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
|
const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
|
|
||||||
|
|
@ -55,8 +63,12 @@ const loadRoutes = async () => {
|
||||||
app.use(session)
|
app.use(session)
|
||||||
|
|
||||||
// Dynamic import for graphql-upload since it's not a CommonJS module
|
// Dynamic import for graphql-upload since it's not a CommonJS module
|
||||||
const { default: graphqlUploadExpress } = await import('graphql-upload/graphqlUploadExpress.mjs')
|
const { default: graphqlUploadExpress } = await import(
|
||||||
const { default: GraphQLUpload } = await import('graphql-upload/GraphQLUpload.mjs')
|
'graphql-upload/graphqlUploadExpress.mjs'
|
||||||
|
)
|
||||||
|
const { default: GraphQLUpload } = await import(
|
||||||
|
'graphql-upload/GraphQLUpload.mjs'
|
||||||
|
)
|
||||||
|
|
||||||
app.use(graphqlUploadExpress())
|
app.use(graphqlUploadExpress())
|
||||||
|
|
||||||
|
|
@ -77,27 +89,31 @@ const loadRoutes = async () => {
|
||||||
plugins: [
|
plugins: [
|
||||||
devMode
|
devMode
|
||||||
? ApolloServerPluginLandingPageLocalDefault()
|
? ApolloServerPluginLandingPageLocalDefault()
|
||||||
: ApolloServerPluginLandingPageDisabled()
|
: ApolloServerPluginLandingPageDisabled(),
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
await apolloServer.start();
|
await apolloServer.start()
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/graphql',
|
'/graphql',
|
||||||
express.json(),
|
express.json(),
|
||||||
expressMiddleware(apolloServer, {
|
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('/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 }))
|
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
|
||||||
|
|
||||||
// Everything not on graphql or api/register is redirected to the front-end
|
// 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
|
return app
|
||||||
}
|
}
|
||||||
|
|
@ -105,10 +121,10 @@ const loadRoutes = async () => {
|
||||||
const certOptions = {
|
const certOptions = {
|
||||||
key: fs.readFileSync(KEY_PATH),
|
key: fs.readFileSync(KEY_PATH),
|
||||||
cert: fs.readFileSync(CERT_PATH),
|
cert: fs.readFileSync(CERT_PATH),
|
||||||
ca: fs.readFileSync(CA_PATH)
|
ca: fs.readFileSync(CA_PATH),
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run () {
|
async function run() {
|
||||||
const app = await loadRoutes()
|
const app = await loadRoutes()
|
||||||
const serverPort = devMode ? 8070 : 443
|
const serverPort = devMode ? 8070 : 443
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
const { ALL } = require('../../plugins/common/ccxt')
|
const { ALL } = require('../../plugins/common/ccxt')
|
||||||
|
|
||||||
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } =
|
||||||
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
|
COINS
|
||||||
|
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } =
|
||||||
|
ALL
|
||||||
|
|
||||||
const TICKER = 'ticker'
|
const TICKER = 'ticker'
|
||||||
const WALLET = 'wallet'
|
const WALLET = 'wallet'
|
||||||
|
|
@ -18,39 +20,142 @@ const WALLET_SCORING = 'wallet_scoring'
|
||||||
const COMPLIANCE = 'compliance'
|
const COMPLIANCE = 'compliance'
|
||||||
|
|
||||||
const ALL_ACCOUNTS = [
|
const ALL_ACCOUNTS = [
|
||||||
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
|
{
|
||||||
{ code: 'bitfinex', display: 'Bitfinex', class: EXCHANGE, cryptos: bitfinex.CRYPTO },
|
code: 'bitfinex',
|
||||||
{ code: 'binance', display: 'Binance', class: TICKER, cryptos: binance.CRYPTO },
|
display: 'Bitfinex',
|
||||||
{ code: 'binanceus', display: 'Binance.us', class: TICKER, cryptos: binanceus.CRYPTO },
|
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: 'cex', display: 'CEX.IO', class: TICKER, cryptos: cex.CRYPTO },
|
||||||
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
|
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
|
||||||
{ code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.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: '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: '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: 'no-layer2',
|
||||||
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
|
display: 'No Layer 2',
|
||||||
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] },
|
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: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||||
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
|
{ 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: '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: '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: 'kraken',
|
||||||
{ code: 'binanceus', display: 'Binance.us', class: EXCHANGE, cryptos: binanceus.CRYPTO },
|
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: '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-wallet',
|
||||||
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
|
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-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: 'twilio', display: 'Twilio', class: SMS },
|
||||||
{ code: 'telnyx', display: 'Telnyx', class: SMS },
|
{ code: 'telnyx', display: 'Telnyx', class: SMS },
|
||||||
{ code: 'vonage', display: 'Vonage', class: SMS },
|
{ code: 'vonage', display: 'Vonage', class: SMS },
|
||||||
|
|
@ -58,17 +163,51 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
|
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
|
||||||
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
|
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
|
||||||
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
{ 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: 'blockcypher',
|
||||||
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX] },
|
display: 'Blockcypher',
|
||||||
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC] },
|
class: ZERO_CONF,
|
||||||
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
|
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: '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 flags = require('minimist')(process.argv.slice(2))
|
||||||
const devMode = flags.dev || flags.lamassuDev
|
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 }
|
module.exports = { ACCOUNT_LIST }
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,255 +1,266 @@
|
||||||
{
|
{
|
||||||
"attribute": {"name":0, "nativeName":1},
|
"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": {
|
||||||
"lang": {
|
"ar": 1,
|
||||||
"aa":["Afar","Afar"],
|
"dv": 1,
|
||||||
"ab":["Abkhazian","Аҧсуа"],
|
"fa": 1,
|
||||||
"af":["Afrikaans","Afrikaans"],
|
"ha": 1,
|
||||||
"ak":["Akan","Akana"],
|
"he": 1,
|
||||||
"am":["Amharic","አማርኛ"],
|
"ks": 1,
|
||||||
"an":["Aragonese","Aragonés"],
|
"ku": 1,
|
||||||
"ar":["Arabic","العربية"],
|
"ps": 1,
|
||||||
"as":["Assamese","অসমীয়া"],
|
"ur": 1,
|
||||||
"av":["Avar","Авар"],
|
"yi": 1
|
||||||
"ay":["Aymara","Aymar"],
|
},
|
||||||
"az":["Azerbaijani","Azərbaycanca / آذربايجان"],
|
"lang": {
|
||||||
"ba":["Bashkir","Башҡорт"],
|
"aa": ["Afar", "Afar"],
|
||||||
"be":["Belarusian","Беларуская"],
|
"ab": ["Abkhazian", "Аҧсуа"],
|
||||||
"bg":["Bulgarian","Български"],
|
"af": ["Afrikaans", "Afrikaans"],
|
||||||
"bh":["Bihari","भोजपुरी"],
|
"ak": ["Akan", "Akana"],
|
||||||
"bi":["Bislama","Bislama"],
|
"am": ["Amharic", "አማርኛ"],
|
||||||
"bm":["Bambara","Bamanankan"],
|
"an": ["Aragonese", "Aragonés"],
|
||||||
"bn":["Bengali","বাংলা"],
|
"ar": ["Arabic", "العربية"],
|
||||||
"bo":["Tibetan","བོད་ཡིག / Bod skad"],
|
"as": ["Assamese", "অসমীয়া"],
|
||||||
"br":["Breton","Brezhoneg"],
|
"av": ["Avar", "Авар"],
|
||||||
"bs":["Bosnian","Bosanski"],
|
"ay": ["Aymara", "Aymar"],
|
||||||
"ca":["Catalan","Català"],
|
"az": ["Azerbaijani", "Azərbaycanca / آذربايجان"],
|
||||||
"ce":["Chechen","Нохчийн"],
|
"ba": ["Bashkir", "Башҡорт"],
|
||||||
"ch":["Chamorro","Chamoru"],
|
"be": ["Belarusian", "Беларуская"],
|
||||||
"co":["Corsican","Corsu"],
|
"bg": ["Bulgarian", "Български"],
|
||||||
"cr":["Cree","Nehiyaw"],
|
"bh": ["Bihari", "भोजपुरी"],
|
||||||
"cs":["Czech","Česky"],
|
"bi": ["Bislama", "Bislama"],
|
||||||
"cu":["Old Church Slavonic / Old Bulgarian","словѣньскъ / slověnĭskŭ"],
|
"bm": ["Bambara", "Bamanankan"],
|
||||||
"cv":["Chuvash","Чăваш"],
|
"bn": ["Bengali", "বাংলা"],
|
||||||
"cy":["Welsh","Cymraeg"],
|
"bo": ["Tibetan", "བོད་ཡིག / Bod skad"],
|
||||||
"da":["Danish","Dansk"],
|
"br": ["Breton", "Brezhoneg"],
|
||||||
"de":["German","Deutsch"],
|
"bs": ["Bosnian", "Bosanski"],
|
||||||
"dv":["Divehi","ދިވެހިބަސް"],
|
"ca": ["Catalan", "Català"],
|
||||||
"dz":["Dzongkha","ཇོང་ཁ"],
|
"ce": ["Chechen", "Нохчийн"],
|
||||||
"ee":["Ewe","Ɛʋɛ"],
|
"ch": ["Chamorro", "Chamoru"],
|
||||||
"el":["Greek","Ελληνικά"],
|
"co": ["Corsican", "Corsu"],
|
||||||
"en":["English","English"],
|
"cr": ["Cree", "Nehiyaw"],
|
||||||
"eo":["Esperanto","Esperanto"],
|
"cs": ["Czech", "Česky"],
|
||||||
"es":["Spanish","Español"],
|
"cu": ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"],
|
||||||
"et":["Estonian","Eesti"],
|
"cv": ["Chuvash", "Чăваш"],
|
||||||
"eu":["Basque","Euskara"],
|
"cy": ["Welsh", "Cymraeg"],
|
||||||
"fa":["Persian","فارسی"],
|
"da": ["Danish", "Dansk"],
|
||||||
"ff":["Peul","Fulfulde"],
|
"de": ["German", "Deutsch"],
|
||||||
"fi":["Finnish","Suomi"],
|
"dv": ["Divehi", "ދިވެހިބަސް"],
|
||||||
"fj":["Fijian","Na Vosa Vakaviti"],
|
"dz": ["Dzongkha", "ཇོང་ཁ"],
|
||||||
"fo":["Faroese","Føroyskt"],
|
"ee": ["Ewe", "Ɛʋɛ"],
|
||||||
"fr":["French","Français"],
|
"el": ["Greek", "Ελληνικά"],
|
||||||
"fy":["West Frisian","Frysk"],
|
"en": ["English", "English"],
|
||||||
"ga":["Irish","Gaeilge"],
|
"eo": ["Esperanto", "Esperanto"],
|
||||||
"gd":["Scottish Gaelic","Gàidhlig"],
|
"es": ["Spanish", "Español"],
|
||||||
"gl":["Galician","Galego"],
|
"et": ["Estonian", "Eesti"],
|
||||||
"gn":["Guarani","Avañe'ẽ"],
|
"eu": ["Basque", "Euskara"],
|
||||||
"gu":["Gujarati","ગુજરાતી"],
|
"fa": ["Persian", "فارسی"],
|
||||||
"gv":["Manx","Gaelg"],
|
"ff": ["Peul", "Fulfulde"],
|
||||||
"ha":["Hausa","هَوُسَ"],
|
"fi": ["Finnish", "Suomi"],
|
||||||
"he":["Hebrew","עברית"],
|
"fj": ["Fijian", "Na Vosa Vakaviti"],
|
||||||
"hi":["Hindi","हिन्दी"],
|
"fo": ["Faroese", "Føroyskt"],
|
||||||
"ho":["Hiri Motu","Hiri Motu"],
|
"fr": ["French", "Français"],
|
||||||
"hr":["Croatian","Hrvatski"],
|
"fy": ["West Frisian", "Frysk"],
|
||||||
"ht":["Haitian","Krèyol ayisyen"],
|
"ga": ["Irish", "Gaeilge"],
|
||||||
"hu":["Hungarian","Magyar"],
|
"gd": ["Scottish Gaelic", "Gàidhlig"],
|
||||||
"hy":["Armenian","Հայերեն"],
|
"gl": ["Galician", "Galego"],
|
||||||
"hz":["Herero","Otsiherero"],
|
"gn": ["Guarani", "Avañe'ẽ"],
|
||||||
"ia":["Interlingua","Interlingua"],
|
"gu": ["Gujarati", "ગુજરાતી"],
|
||||||
"id":["Indonesian","Bahasa Indonesia"],
|
"gv": ["Manx", "Gaelg"],
|
||||||
"ie":["Interlingue","Interlingue"],
|
"ha": ["Hausa", "هَوُسَ"],
|
||||||
"ig":["Igbo","Igbo"],
|
"he": ["Hebrew", "עברית"],
|
||||||
"ii":["Sichuan Yi","ꆇꉙ / 四川彝语"],
|
"hi": ["Hindi", "हिन्दी"],
|
||||||
"ik":["Inupiak","Iñupiak"],
|
"ho": ["Hiri Motu", "Hiri Motu"],
|
||||||
"io":["Ido","Ido"],
|
"hr": ["Croatian", "Hrvatski"],
|
||||||
"is":["Icelandic","Íslenska"],
|
"ht": ["Haitian", "Krèyol ayisyen"],
|
||||||
"it":["Italian","Italiano"],
|
"hu": ["Hungarian", "Magyar"],
|
||||||
"iu":["Inuktitut","ᐃᓄᒃᑎᑐᑦ"],
|
"hy": ["Armenian", "Հայերեն"],
|
||||||
"ja":["Japanese","日本語"],
|
"hz": ["Herero", "Otsiherero"],
|
||||||
"jv":["Javanese","Basa Jawa"],
|
"ia": ["Interlingua", "Interlingua"],
|
||||||
"ka":["Georgian","ქართული"],
|
"id": ["Indonesian", "Bahasa Indonesia"],
|
||||||
"kg":["Kongo","KiKongo"],
|
"ie": ["Interlingue", "Interlingue"],
|
||||||
"ki":["Kikuyu","Gĩkũyũ"],
|
"ig": ["Igbo", "Igbo"],
|
||||||
"kj":["Kuanyama","Kuanyama"],
|
"ii": ["Sichuan Yi", "ꆇꉙ / 四川彝语"],
|
||||||
"kk":["Kazakh","Қазақша"],
|
"ik": ["Inupiak", "Iñupiak"],
|
||||||
"kl":["Greenlandic","Kalaallisut"],
|
"io": ["Ido", "Ido"],
|
||||||
"km":["Cambodian","ភាសាខ្មែរ"],
|
"is": ["Icelandic", "Íslenska"],
|
||||||
"kn":["Kannada","ಕನ್ನಡ"],
|
"it": ["Italian", "Italiano"],
|
||||||
"ko":["Korean","한국어"],
|
"iu": ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"],
|
||||||
"kr":["Kanuri","Kanuri"],
|
"ja": ["Japanese", "日本語"],
|
||||||
"ks":["Kashmiri","कश्मीरी / كشميري"],
|
"jv": ["Javanese", "Basa Jawa"],
|
||||||
"ku":["Kurdish","Kurdî / كوردی"],
|
"ka": ["Georgian", "ქართული"],
|
||||||
"kv":["Komi","Коми"],
|
"kg": ["Kongo", "KiKongo"],
|
||||||
"kw":["Cornish","Kernewek"],
|
"ki": ["Kikuyu", "Gĩkũyũ"],
|
||||||
"ky":["Kirghiz","Kırgızca / Кыргызча"],
|
"kj": ["Kuanyama", "Kuanyama"],
|
||||||
"la":["Latin","Latina"],
|
"kk": ["Kazakh", "Қазақша"],
|
||||||
"lb":["Luxembourgish","Lëtzebuergesch"],
|
"kl": ["Greenlandic", "Kalaallisut"],
|
||||||
"lg":["Ganda","Luganda"],
|
"km": ["Cambodian", "ភាសាខ្មែរ"],
|
||||||
"li":["Limburgian","Limburgs"],
|
"kn": ["Kannada", "ಕನ್ನಡ"],
|
||||||
"ln":["Lingala","Lingála"],
|
"ko": ["Korean", "한국어"],
|
||||||
"lo":["Laotian","ລາວ / Pha xa lao"],
|
"kr": ["Kanuri", "Kanuri"],
|
||||||
"lt":["Lithuanian","Lietuvių"],
|
"ks": ["Kashmiri", "कश्मीरी / كشميري"],
|
||||||
"lv":["Latvian","Latviešu"],
|
"ku": ["Kurdish", "Kurdî / كوردی"],
|
||||||
"mg":["Malagasy","Malagasy"],
|
"kv": ["Komi", "Коми"],
|
||||||
"mh":["Marshallese","Kajin Majel / Ebon"],
|
"kw": ["Cornish", "Kernewek"],
|
||||||
"mi":["Maori","Māori"],
|
"ky": ["Kirghiz", "Kırgızca / Кыргызча"],
|
||||||
"mk":["Macedonian","Македонски"],
|
"la": ["Latin", "Latina"],
|
||||||
"ml":["Malayalam","മലയാളം"],
|
"lb": ["Luxembourgish", "Lëtzebuergesch"],
|
||||||
"mn":["Mongolian","Монгол"],
|
"lg": ["Ganda", "Luganda"],
|
||||||
"mo":["Moldovan","Moldovenească"],
|
"li": ["Limburgian", "Limburgs"],
|
||||||
"mr":["Marathi","मराठी"],
|
"ln": ["Lingala", "Lingála"],
|
||||||
"ms":["Malay","Bahasa Melayu"],
|
"lo": ["Laotian", "ລາວ / Pha xa lao"],
|
||||||
"mt":["Maltese","bil-Malti"],
|
"lt": ["Lithuanian", "Lietuvių"],
|
||||||
"my":["Burmese","Myanmasa"],
|
"lv": ["Latvian", "Latviešu"],
|
||||||
"na":["Nauruan","Dorerin Naoero"],
|
"mg": ["Malagasy", "Malagasy"],
|
||||||
"nd":["North Ndebele","Sindebele"],
|
"mh": ["Marshallese", "Kajin Majel / Ebon"],
|
||||||
"ne":["Nepali","नेपाली"],
|
"mi": ["Maori", "Māori"],
|
||||||
"ng":["Ndonga","Oshiwambo"],
|
"mk": ["Macedonian", "Македонски"],
|
||||||
"nl":["Dutch","Nederlands"],
|
"ml": ["Malayalam", "മലയാളം"],
|
||||||
"nn":["Norwegian Nynorsk","Norsk (nynorsk)"],
|
"mn": ["Mongolian", "Монгол"],
|
||||||
"no":["Norwegian","Norsk (bokmål / riksmål)"],
|
"mo": ["Moldovan", "Moldovenească"],
|
||||||
"nr":["South Ndebele","isiNdebele"],
|
"mr": ["Marathi", "मराठी"],
|
||||||
"nv":["Navajo","Diné bizaad"],
|
"ms": ["Malay", "Bahasa Melayu"],
|
||||||
"ny":["Chichewa","Chi-Chewa"],
|
"mt": ["Maltese", "bil-Malti"],
|
||||||
"oc":["Occitan","Occitan"],
|
"my": ["Burmese", "Myanmasa"],
|
||||||
"oj":["Ojibwa","ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
|
"na": ["Nauruan", "Dorerin Naoero"],
|
||||||
"om":["Oromo","Oromoo"],
|
"nd": ["North Ndebele", "Sindebele"],
|
||||||
"or":["Oriya","ଓଡ଼ିଆ"],
|
"ne": ["Nepali", "नेपाली"],
|
||||||
"os":["Ossetian / Ossetic","Иронау"],
|
"ng": ["Ndonga", "Oshiwambo"],
|
||||||
"pa":["Panjabi / Punjabi","ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
|
"nl": ["Dutch", "Nederlands"],
|
||||||
"pi":["Pali","Pāli / पाऴि"],
|
"nn": ["Norwegian Nynorsk", "Norsk (nynorsk)"],
|
||||||
"pl":["Polish","Polski"],
|
"no": ["Norwegian", "Norsk (bokmål / riksmål)"],
|
||||||
"ps":["Pashto","پښتو"],
|
"nr": ["South Ndebele", "isiNdebele"],
|
||||||
"pt":["Portuguese","Português"],
|
"nv": ["Navajo", "Diné bizaad"],
|
||||||
"qu":["Quechua","Runa Simi"],
|
"ny": ["Chichewa", "Chi-Chewa"],
|
||||||
"rm":["Raeto Romance","Rumantsch"],
|
"oc": ["Occitan", "Occitan"],
|
||||||
"rn":["Kirundi","Kirundi"],
|
"oj": ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
|
||||||
"ro":["Romanian","Română"],
|
"om": ["Oromo", "Oromoo"],
|
||||||
"ru":["Russian","Русский"],
|
"or": ["Oriya", "ଓଡ଼ିଆ"],
|
||||||
"rw":["Rwandi","Kinyarwandi"],
|
"os": ["Ossetian / Ossetic", "Иронау"],
|
||||||
"sa":["Sanskrit","संस्कृतम्"],
|
"pa": ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
|
||||||
"sc":["Sardinian","Sardu"],
|
"pi": ["Pali", "Pāli / पाऴि"],
|
||||||
"sd":["Sindhi","सिनधि"],
|
"pl": ["Polish", "Polski"],
|
||||||
"se":["Northern Sami","Sámegiella"],
|
"ps": ["Pashto", "پښتو"],
|
||||||
"sg":["Sango","Sängö"],
|
"pt": ["Portuguese", "Português"],
|
||||||
"sh":["Serbo-Croatian","Srpskohrvatski / Српскохрватски"],
|
"qu": ["Quechua", "Runa Simi"],
|
||||||
"si":["Sinhalese","සිංහල"],
|
"rm": ["Raeto Romance", "Rumantsch"],
|
||||||
"sk":["Slovak","Slovenčina"],
|
"rn": ["Kirundi", "Kirundi"],
|
||||||
"sl":["Slovenian","Slovenščina"],
|
"ro": ["Romanian", "Română"],
|
||||||
"sm":["Samoan","Gagana Samoa"],
|
"ru": ["Russian", "Русский"],
|
||||||
"sn":["Shona","chiShona"],
|
"rw": ["Rwandi", "Kinyarwandi"],
|
||||||
"so":["Somalia","Soomaaliga"],
|
"sa": ["Sanskrit", "संस्कृतम्"],
|
||||||
"sq":["Albanian","Shqip"],
|
"sc": ["Sardinian", "Sardu"],
|
||||||
"sr":["Serbian","Српски"],
|
"sd": ["Sindhi", "सिनधि"],
|
||||||
"ss":["Swati","SiSwati"],
|
"se": ["Northern Sami", "Sámegiella"],
|
||||||
"st":["Southern Sotho","Sesotho"],
|
"sg": ["Sango", "Sängö"],
|
||||||
"su":["Sundanese","Basa Sunda"],
|
"sh": ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"],
|
||||||
"sv":["Swedish","Svenska"],
|
"si": ["Sinhalese", "සිංහල"],
|
||||||
"sw":["Swahili","Kiswahili"],
|
"sk": ["Slovak", "Slovenčina"],
|
||||||
"ta":["Tamil","தமிழ்"],
|
"sl": ["Slovenian", "Slovenščina"],
|
||||||
"te":["Telugu","తెలుగు"],
|
"sm": ["Samoan", "Gagana Samoa"],
|
||||||
"tg":["Tajik","Тоҷикӣ"],
|
"sn": ["Shona", "chiShona"],
|
||||||
"th":["Thai","ไทย / Phasa Thai"],
|
"so": ["Somalia", "Soomaaliga"],
|
||||||
"ti":["Tigrinya","ትግርኛ"],
|
"sq": ["Albanian", "Shqip"],
|
||||||
"tk":["Turkmen","Туркмен / تركمن"],
|
"sr": ["Serbian", "Српски"],
|
||||||
"tl":["Tagalog / Filipino","Tagalog"],
|
"ss": ["Swati", "SiSwati"],
|
||||||
"tn":["Tswana","Setswana"],
|
"st": ["Southern Sotho", "Sesotho"],
|
||||||
"to":["Tonga","Lea Faka-Tonga"],
|
"su": ["Sundanese", "Basa Sunda"],
|
||||||
"tr":["Turkish","Türkçe"],
|
"sv": ["Swedish", "Svenska"],
|
||||||
"ts":["Tsonga","Xitsonga"],
|
"sw": ["Swahili", "Kiswahili"],
|
||||||
"tt":["Tatar","Tatarça"],
|
"ta": ["Tamil", "தமிழ்"],
|
||||||
"tw":["Twi","Twi"],
|
"te": ["Telugu", "తెలుగు"],
|
||||||
"ty":["Tahitian","Reo Mā`ohi"],
|
"tg": ["Tajik", "Тоҷикӣ"],
|
||||||
"ug":["Uyghur","Uyƣurqə / ئۇيغۇرچە"],
|
"th": ["Thai", "ไทย / Phasa Thai"],
|
||||||
"uk":["Ukrainian","Українська"],
|
"ti": ["Tigrinya", "ትግርኛ"],
|
||||||
"ur":["Urdu","اردو"],
|
"tk": ["Turkmen", "Туркмен / تركمن"],
|
||||||
"uz":["Uzbek","Ўзбек"],
|
"tl": ["Tagalog / Filipino", "Tagalog"],
|
||||||
"ve":["Venda","Tshivenḓa"],
|
"tn": ["Tswana", "Setswana"],
|
||||||
"vi":["Vietnamese","Tiếng Việt"],
|
"to": ["Tonga", "Lea Faka-Tonga"],
|
||||||
"vo":["Volapük","Volapük"],
|
"tr": ["Turkish", "Türkçe"],
|
||||||
"wa":["Walloon","Walon"],
|
"ts": ["Tsonga", "Xitsonga"],
|
||||||
"wo":["Wolof","Wollof"],
|
"tt": ["Tatar", "Tatarça"],
|
||||||
"xh":["Xhosa","isiXhosa"],
|
"tw": ["Twi", "Twi"],
|
||||||
"yi":["Yiddish","ייִדיש"],
|
"ty": ["Tahitian", "Reo Mā`ohi"],
|
||||||
"yo":["Yoruba","Yorùbá"],
|
"ug": ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"],
|
||||||
"za":["Zhuang","Cuengh / Tôô / 壮语"],
|
"uk": ["Ukrainian", "Українська"],
|
||||||
"zh":["Chinese","中文"],
|
"ur": ["Urdu", "اردو"],
|
||||||
"zu":["Zulu","isiZulu"]
|
"uz": ["Uzbek", "Ўзбек"],
|
||||||
},
|
"ve": ["Venda", "Tshivenḓa"],
|
||||||
"supported": [
|
"vi": ["Vietnamese", "Tiếng Việt"],
|
||||||
"en-US",
|
"vo": ["Volapük", "Volapük"],
|
||||||
"en-CA",
|
"wa": ["Walloon", "Walon"],
|
||||||
"fr-QC",
|
"wo": ["Wolof", "Wollof"],
|
||||||
"ach-UG",
|
"xh": ["Xhosa", "isiXhosa"],
|
||||||
"af-ZA",
|
"yi": ["Yiddish", "ייִדיש"],
|
||||||
"ar-SA",
|
"yo": ["Yoruba", "Yorùbá"],
|
||||||
"bg-BG",
|
"za": ["Zhuang", "Cuengh / Tôô / 壮语"],
|
||||||
"ca-ES",
|
"zh": ["Chinese", "中文"],
|
||||||
"cs-CZ",
|
"zu": ["Zulu", "isiZulu"]
|
||||||
"cy-GB",
|
},
|
||||||
"de-DE",
|
"supported": [
|
||||||
"de-AT",
|
"en-US",
|
||||||
"de-CH",
|
"en-CA",
|
||||||
"da-DK",
|
"fr-QC",
|
||||||
"el-GR",
|
"ach-UG",
|
||||||
"en-GB",
|
"af-ZA",
|
||||||
"en-AU",
|
"ar-SA",
|
||||||
"en-HK",
|
"bg-BG",
|
||||||
"en-IE",
|
"ca-ES",
|
||||||
"en-NZ",
|
"cs-CZ",
|
||||||
"en-PR",
|
"cy-GB",
|
||||||
"es-ES",
|
"de-DE",
|
||||||
"es-MX",
|
"de-AT",
|
||||||
"et-EE",
|
"de-CH",
|
||||||
"fi-FI",
|
"da-DK",
|
||||||
"fr-FR",
|
"el-GR",
|
||||||
"fr-CH",
|
"en-GB",
|
||||||
"fur-IT",
|
"en-AU",
|
||||||
"ga-IE",
|
"en-HK",
|
||||||
"gd-GB",
|
"en-IE",
|
||||||
"he-IL",
|
"en-NZ",
|
||||||
"hr-HR",
|
"en-PR",
|
||||||
"hu-HU",
|
"es-ES",
|
||||||
"hy-AM",
|
"es-MX",
|
||||||
"id-ID",
|
"et-EE",
|
||||||
"it-CH",
|
"fi-FI",
|
||||||
"it-IT",
|
"fr-FR",
|
||||||
"ja-JP",
|
"fr-CH",
|
||||||
"ka-GE",
|
"fur-IT",
|
||||||
"ko-KR",
|
"ga-IE",
|
||||||
"ky-KG",
|
"gd-GB",
|
||||||
"lt-LT",
|
"he-IL",
|
||||||
"nb-NO",
|
"hr-HR",
|
||||||
"nl-BE",
|
"hu-HU",
|
||||||
"nl-NL",
|
"hy-AM",
|
||||||
"pt-PT",
|
"id-ID",
|
||||||
"pt-BR",
|
"it-CH",
|
||||||
"pl-PL",
|
"it-IT",
|
||||||
"ro-RO",
|
"ja-JP",
|
||||||
"ru-RU",
|
"ka-GE",
|
||||||
"sco-GB",
|
"ko-KR",
|
||||||
"sh-HR",
|
"ky-KG",
|
||||||
"sk-SK",
|
"lt-LT",
|
||||||
"sl-SI",
|
"nb-NO",
|
||||||
"sr-SP",
|
"nl-BE",
|
||||||
"sv-SE",
|
"nl-NL",
|
||||||
"th-TH",
|
"pt-PT",
|
||||||
"tr-TR",
|
"pt-BR",
|
||||||
"uk-UA",
|
"pl-PL",
|
||||||
"vi-VN",
|
"ro-RO",
|
||||||
"zh-CN",
|
"ru-RU",
|
||||||
"zh-HK",
|
"sco-GB",
|
||||||
"zh-SG",
|
"sh-HR",
|
||||||
"zh-TW"
|
"sk-SK",
|
||||||
]
|
"sl-SI",
|
||||||
|
"sr-SP",
|
||||||
|
"sv-SE",
|
||||||
|
"th-TH",
|
||||||
|
"tr-TR",
|
||||||
|
"uk-UA",
|
||||||
|
"vi-VN",
|
||||||
|
"zh-CN",
|
||||||
|
"zh-HK",
|
||||||
|
"zh-SG",
|
||||||
|
"zh-TW"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ const countries = require('./data/countries.json')
|
||||||
const currenciesRec = require('./data/currencies.json')
|
const currenciesRec = require('./data/currencies.json')
|
||||||
const languageRec = require('./data/languages.json')
|
const languageRec = require('./data/languages.json')
|
||||||
|
|
||||||
function massageCurrencies (currencies) {
|
function massageCurrencies(currencies) {
|
||||||
const convert = r => ({
|
const convert = r => ({
|
||||||
code: r['Alphabetic Code'],
|
code: r['Alphabetic Code'],
|
||||||
display: r['Currency']
|
display: r['Currency'],
|
||||||
})
|
})
|
||||||
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
|
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
|
||||||
const mapped = _.map(convert, currencies)
|
const mapped = _.map(convert, currencies)
|
||||||
|
|
@ -37,7 +37,7 @@ const massageCryptos = cryptos => {
|
||||||
code: crypto['cryptoCode'],
|
code: crypto['cryptoCode'],
|
||||||
display: crypto['display'],
|
display: crypto['display'],
|
||||||
codeDisplay: crypto['cryptoCodeDisplay'] ?? crypto['cryptoCode'],
|
codeDisplay: crypto['cryptoCodeDisplay'] ?? crypto['cryptoCode'],
|
||||||
isBeta: betaList.includes(crypto.cryptoCode)
|
isBeta: betaList.includes(crypto.cryptoCode),
|
||||||
})
|
})
|
||||||
|
|
||||||
return _.map(convert, cryptos)
|
return _.map(convert, cryptos)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ const db = require('../db')
|
||||||
const cashInTx = require('../cash-in/cash-in-tx')
|
const cashInTx = require('../cash-in/cash-in-tx')
|
||||||
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
||||||
|
|
||||||
function transaction () {
|
function transaction() {
|
||||||
const sql = `SELECT DISTINCT * FROM (
|
const sql = `SELECT DISTINCT * FROM (
|
||||||
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
|
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
|
||||||
SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION
|
SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION
|
||||||
|
|
@ -27,7 +27,7 @@ function transaction () {
|
||||||
|
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
}
|
}
|
||||||
function customer () {
|
function customer() {
|
||||||
const sql = `SELECT DISTINCT * FROM (
|
const sql = `SELECT DISTINCT * FROM (
|
||||||
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
||||||
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION
|
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const { AuthenticationError } = require('../errors')
|
||||||
function authDirectiveTransformer(schema, directiveName = 'auth') {
|
function authDirectiveTransformer(schema, directiveName = 'auth') {
|
||||||
return mapSchema(schema, {
|
return mapSchema(schema, {
|
||||||
// For object types
|
// For object types
|
||||||
[MapperKind.OBJECT_TYPE]: (objectType) => {
|
[MapperKind.OBJECT_TYPE]: objectType => {
|
||||||
const directive = getDirective(schema, objectType, directiveName)?.[0]
|
const directive = getDirective(schema, objectType, directiveName)?.[0]
|
||||||
if (directive) {
|
if (directive) {
|
||||||
const requiredAuthRole = directive.requires
|
const requiredAuthRole = directive.requires
|
||||||
|
|
@ -30,19 +30,23 @@ function authDirectiveTransformer(schema, directiveName = 'auth') {
|
||||||
// Apply auth check to the field's resolver
|
// Apply auth check to the field's resolver
|
||||||
const { resolve = defaultFieldResolver } = fieldConfig
|
const { resolve = defaultFieldResolver } = fieldConfig
|
||||||
fieldConfig.resolve = function (root, args, context, info) {
|
fieldConfig.resolve = function (root, args, context, info) {
|
||||||
const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole
|
const requiredRoles =
|
||||||
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
|
fieldConfig._requiredAuthRole || objectType._requiredAuthRole
|
||||||
|
if (!requiredRoles)
|
||||||
|
return resolve.apply(this, [root, args, context, info])
|
||||||
|
|
||||||
const user = context.req.session.user
|
const user = context.req.session.user
|
||||||
if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) {
|
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 resolve.apply(this, [root, args, context, info])
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldConfig
|
return fieldConfig
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ class AuthenticationError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Authentication failed', {
|
super('Authentication failed', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'UNAUTHENTICATED'
|
code: 'UNAUTHENTICATED',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,8 +15,8 @@ class InvalidCredentialsError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Invalid credentials', {
|
super('Invalid credentials', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'INVALID_CREDENTIALS'
|
code: 'INVALID_CREDENTIALS',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,8 +25,8 @@ class UserAlreadyExistsError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('User already exists', {
|
super('User already exists', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'USER_ALREADY_EXISTS'
|
code: 'USER_ALREADY_EXISTS',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,8 +35,8 @@ class InvalidTwoFactorError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Invalid two-factor code', {
|
super('Invalid two-factor code', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'INVALID_TWO_FACTOR_CODE'
|
code: 'INVALID_TWO_FACTOR_CODE',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +45,8 @@ class InvalidUrlError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Invalid URL token', {
|
super('Invalid URL token', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: 'INVALID_URL_TOKEN'
|
code: 'INVALID_URL_TOKEN',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,8 +55,8 @@ class UserInputError extends GraphQLError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('User input error', {
|
super('User input error', {
|
||||||
extensions: {
|
extensions: {
|
||||||
code: ApolloServerErrorCode.BAD_USER_INPUT
|
code: ApolloServerErrorCode.BAD_USER_INPUT,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,5 +67,5 @@ module.exports = {
|
||||||
UserAlreadyExistsError,
|
UserAlreadyExistsError,
|
||||||
InvalidTwoFactorError,
|
InvalidTwoFactorError,
|
||||||
InvalidUrlError,
|
InvalidUrlError,
|
||||||
UserInputError
|
UserInputError,
|
||||||
}
|
}
|
||||||
|
|
@ -12,60 +12,70 @@ const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
const REMEMBER_ME_AGE = 90 * T.day
|
const REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
||||||
const generateAttestationOptions = (session, options) => {
|
const generateAttestationOptions = (session, options) => {
|
||||||
return users.getUserById(options.userId).then(user => {
|
return users
|
||||||
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
|
.getUserById(options.userId)
|
||||||
}).then(([userDevices, user]) => {
|
.then(user => {
|
||||||
const opts = simpleWebauthn.generateAttestationOptions({
|
return Promise.all([
|
||||||
rpName: 'Lamassu',
|
credentials.getHardwareCredentialsByUserId(user.id),
|
||||||
rpID: options.domain,
|
user,
|
||||||
userName: user.username,
|
])
|
||||||
userID: user.id,
|
|
||||||
timeout: 60000,
|
|
||||||
attestationType: 'indirect',
|
|
||||||
excludeCredentials: userDevices.map(dev => ({
|
|
||||||
id: dev.data.credentialID,
|
|
||||||
type: 'public-key',
|
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
|
||||||
})),
|
|
||||||
authenticatorSelection: {
|
|
||||||
userVerification: 'discouraged',
|
|
||||||
requireResidentKey: false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.then(([userDevices, user]) => {
|
||||||
session.webauthn = {
|
const opts = simpleWebauthn.generateAttestationOptions({
|
||||||
attestation: {
|
rpName: 'Lamassu',
|
||||||
challenge: opts.challenge
|
rpID: options.domain,
|
||||||
}
|
userName: user.username,
|
||||||
}
|
userID: user.id,
|
||||||
|
|
||||||
return opts
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAssertionOptions = (session, options) => {
|
|
||||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
|
||||||
const opts = simpleWebauthn.generateAssertionOptions({
|
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
allowCredentials: devices.map(dev => ({
|
attestationType: 'indirect',
|
||||||
|
excludeCredentials: userDevices.map(dev => ({
|
||||||
id: dev.data.credentialID,
|
id: dev.data.credentialID,
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
})),
|
})),
|
||||||
userVerification: 'discouraged',
|
authenticatorSelection: {
|
||||||
rpID: options.domain
|
userVerification: 'discouraged',
|
||||||
|
requireResidentKey: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
session.webauthn = {
|
session.webauthn = {
|
||||||
assertion: {
|
attestation: {
|
||||||
challenge: opts.challenge
|
challenge: opts.challenge,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const generateAssertionOptions = (session, options) => {
|
||||||
|
return userManagement
|
||||||
|
.authenticateUser(options.username, options.password)
|
||||||
|
.then(user => {
|
||||||
|
return credentials
|
||||||
|
.getHardwareCredentialsByUserId(user.id)
|
||||||
|
.then(devices => {
|
||||||
|
const opts = simpleWebauthn.generateAssertionOptions({
|
||||||
|
timeout: 60000,
|
||||||
|
allowCredentials: devices.map(dev => ({
|
||||||
|
id: dev.data.credentialID,
|
||||||
|
type: 'public-key',
|
||||||
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
|
})),
|
||||||
|
userVerification: 'discouraged',
|
||||||
|
rpID: options.domain,
|
||||||
|
})
|
||||||
|
|
||||||
|
session.webauthn = {
|
||||||
|
assertion: {
|
||||||
|
challenge: opts.challenge,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAttestation = (session, options) => {
|
const validateAttestation = (session, options) => {
|
||||||
|
|
@ -78,98 +88,112 @@ const validateAttestation = (session, options) => {
|
||||||
credential: options.attestationResponse,
|
credential: options.attestationResponse,
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain
|
expectedRPID: options.domain,
|
||||||
})
|
}),
|
||||||
])
|
]).then(([user, verification]) => {
|
||||||
.then(([user, verification]) => {
|
const { verified, attestationInfo } = verification
|
||||||
const { verified, attestationInfo } = verification
|
|
||||||
|
|
||||||
if (!(verified || attestationInfo)) {
|
if (!(verified || attestationInfo)) {
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { counter, credentialPublicKey, credentialID } = attestationInfo
|
||||||
counter,
|
|
||||||
credentialPublicKey,
|
|
||||||
credentialID
|
|
||||||
} = attestationInfo
|
|
||||||
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
return credentials
|
||||||
.then(userDevices => {
|
.getHardwareCredentialsByUserId(user.id)
|
||||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
.then(userDevices => {
|
||||||
|
const existingDevice = userDevices.find(
|
||||||
|
device => device.data.credentialID === credentialID,
|
||||||
|
)
|
||||||
|
|
||||||
if (!existingDevice) {
|
if (!existingDevice) {
|
||||||
const newDevice = {
|
const newDevice = {
|
||||||
counter,
|
counter,
|
||||||
credentialPublicKey,
|
credentialPublicKey,
|
||||||
credentialID
|
credentialID,
|
||||||
}
|
|
||||||
credentials.createHardwareCredential(user.id, newDevice)
|
|
||||||
}
|
}
|
||||||
|
credentials.createHardwareCredential(user.id, newDevice)
|
||||||
|
}
|
||||||
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAssertion = (session, options) => {
|
const validateAssertion = (session, options) => {
|
||||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
return userManagement
|
||||||
const expectedChallenge = session.webauthn.assertion.challenge
|
.authenticateUser(options.username, options.password)
|
||||||
|
.then(user => {
|
||||||
|
const expectedChallenge = session.webauthn.assertion.challenge
|
||||||
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
return credentials
|
||||||
const dbAuthenticator = _.find(dev => {
|
.getHardwareCredentialsByUserId(user.id)
|
||||||
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
|
.then(devices => {
|
||||||
}, devices)
|
const dbAuthenticator = _.find(dev => {
|
||||||
|
return (
|
||||||
|
Buffer.from(dev.data.credentialID).compare(
|
||||||
|
base64url.toBuffer(options.assertionResponse.rawId),
|
||||||
|
) === 0
|
||||||
|
)
|
||||||
|
}, devices)
|
||||||
|
|
||||||
if (!dbAuthenticator.data) {
|
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(
|
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
|
||||||
dbAuthenticator.data,
|
credentialPublicKey: Buffer.from(
|
||||||
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
|
dbAuthenticator.data.credentialPublicKey,
|
||||||
)
|
),
|
||||||
|
})
|
||||||
|
|
||||||
let verification
|
let verification
|
||||||
try {
|
try {
|
||||||
verification = simpleWebauthn.verifyAssertionResponse({
|
verification = simpleWebauthn.verifyAssertionResponse({
|
||||||
credential: options.assertionResponse,
|
credential: options.assertionResponse,
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain,
|
expectedRPID: options.domain,
|
||||||
authenticator: convertedAuthenticator
|
authenticator: convertedAuthenticator,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, assertionInfo } = verification
|
const { verified, assertionInfo } = verification
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
dbAuthenticator.data.counter = assertionInfo.newCounter
|
dbAuthenticator.data.counter = assertionInfo.newCounter
|
||||||
return credentials.updateHardwareCredential(dbAuthenticator)
|
return credentials
|
||||||
.then(() => {
|
.updateHardwareCredential(dbAuthenticator)
|
||||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
.then(() => {
|
||||||
session.user = finalUser
|
const finalUser = {
|
||||||
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
}
|
||||||
|
session.user = finalUser
|
||||||
|
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
||||||
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateAttestationOptions,
|
generateAttestationOptions,
|
||||||
generateAssertionOptions,
|
generateAssertionOptions,
|
||||||
validateAttestation,
|
validateAttestation,
|
||||||
validateAssertion
|
validateAssertion,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,35 +11,41 @@ const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
const REMEMBER_ME_AGE = 90 * T.day
|
const REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
||||||
const generateAttestationOptions = (session, options) => {
|
const generateAttestationOptions = (session, options) => {
|
||||||
return users.getUserById(options.userId).then(user => {
|
return users
|
||||||
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
|
.getUserById(options.userId)
|
||||||
}).then(([userDevices, user]) => {
|
.then(user => {
|
||||||
const opts = simpleWebauthn.generateAttestationOptions({
|
return Promise.all([
|
||||||
rpName: 'Lamassu',
|
credentials.getHardwareCredentialsByUserId(user.id),
|
||||||
rpID: options.domain,
|
user,
|
||||||
userName: user.username,
|
])
|
||||||
userID: user.id,
|
|
||||||
timeout: 60000,
|
|
||||||
attestationType: 'indirect',
|
|
||||||
excludeCredentials: userDevices.map(dev => ({
|
|
||||||
id: dev.data.credentialID,
|
|
||||||
type: 'public-key',
|
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
|
||||||
})),
|
|
||||||
authenticatorSelection: {
|
|
||||||
userVerification: 'discouraged',
|
|
||||||
requireResidentKey: false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.then(([userDevices, user]) => {
|
||||||
|
const opts = simpleWebauthn.generateAttestationOptions({
|
||||||
|
rpName: 'Lamassu',
|
||||||
|
rpID: options.domain,
|
||||||
|
userName: user.username,
|
||||||
|
userID: user.id,
|
||||||
|
timeout: 60000,
|
||||||
|
attestationType: 'indirect',
|
||||||
|
excludeCredentials: userDevices.map(dev => ({
|
||||||
|
id: dev.data.credentialID,
|
||||||
|
type: 'public-key',
|
||||||
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
|
})),
|
||||||
|
authenticatorSelection: {
|
||||||
|
userVerification: 'discouraged',
|
||||||
|
requireResidentKey: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
session.webauthn = {
|
session.webauthn = {
|
||||||
attestation: {
|
attestation: {
|
||||||
challenge: opts.challenge
|
challenge: opts.challenge,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateAssertionOptions = (session, options) => {
|
const generateAssertionOptions = (session, options) => {
|
||||||
|
|
@ -50,16 +56,16 @@ const generateAssertionOptions = (session, options) => {
|
||||||
allowCredentials: devices.map(dev => ({
|
allowCredentials: devices.map(dev => ({
|
||||||
id: dev.data.credentialID,
|
id: dev.data.credentialID,
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
})),
|
})),
|
||||||
userVerification: 'discouraged',
|
userVerification: 'discouraged',
|
||||||
rpID: options.domain
|
rpID: options.domain,
|
||||||
})
|
})
|
||||||
|
|
||||||
session.webauthn = {
|
session.webauthn = {
|
||||||
assertion: {
|
assertion: {
|
||||||
challenge: opts.challenge
|
challenge: opts.challenge,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
|
|
@ -77,40 +83,38 @@ const validateAttestation = (session, options) => {
|
||||||
credential: options.attestationResponse,
|
credential: options.attestationResponse,
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain
|
expectedRPID: options.domain,
|
||||||
})
|
}),
|
||||||
])
|
]).then(([user, verification]) => {
|
||||||
.then(([user, verification]) => {
|
const { verified, attestationInfo } = verification
|
||||||
const { verified, attestationInfo } = verification
|
|
||||||
|
|
||||||
if (!(verified || attestationInfo)) {
|
if (!(verified || attestationInfo)) {
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { counter, credentialPublicKey, credentialID } = attestationInfo
|
||||||
counter,
|
|
||||||
credentialPublicKey,
|
|
||||||
credentialID
|
|
||||||
} = attestationInfo
|
|
||||||
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
return credentials
|
||||||
.then(userDevices => {
|
.getHardwareCredentialsByUserId(user.id)
|
||||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
.then(userDevices => {
|
||||||
|
const existingDevice = userDevices.find(
|
||||||
|
device => device.data.credentialID === credentialID,
|
||||||
|
)
|
||||||
|
|
||||||
if (!existingDevice) {
|
if (!existingDevice) {
|
||||||
const newDevice = {
|
const newDevice = {
|
||||||
counter,
|
counter,
|
||||||
credentialPublicKey,
|
credentialPublicKey,
|
||||||
credentialID
|
credentialID,
|
||||||
}
|
|
||||||
credentials.createHardwareCredential(user.id, newDevice)
|
|
||||||
}
|
}
|
||||||
|
credentials.createHardwareCredential(user.id, newDevice)
|
||||||
|
}
|
||||||
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAssertion = (session, options) => {
|
const validateAssertion = (session, options) => {
|
||||||
|
|
@ -119,17 +123,24 @@ const validateAssertion = (session, options) => {
|
||||||
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
||||||
const dbAuthenticator = _.find(dev => {
|
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)
|
}, devices)
|
||||||
|
|
||||||
if (!dbAuthenticator.data) {
|
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(
|
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
|
||||||
dbAuthenticator.data,
|
credentialPublicKey: Buffer.from(
|
||||||
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
|
dbAuthenticator.data.credentialPublicKey,
|
||||||
)
|
),
|
||||||
|
})
|
||||||
|
|
||||||
let verification
|
let verification
|
||||||
try {
|
try {
|
||||||
|
|
@ -138,7 +149,7 @@ const validateAssertion = (session, options) => {
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain,
|
expectedRPID: options.domain,
|
||||||
authenticator: convertedAuthenticator
|
authenticator: convertedAuthenticator,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
@ -148,20 +159,22 @@ const validateAssertion = (session, options) => {
|
||||||
const { verified, assertionInfo } = verification
|
const { verified, assertionInfo } = verification
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
context.req.session.webauthn = null
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
dbAuthenticator.data.counter = assertionInfo.newCounter
|
dbAuthenticator.data.counter = assertionInfo.newCounter
|
||||||
return credentials.updateHardwareCredential(dbAuthenticator)
|
return credentials.updateHardwareCredential(dbAuthenticator).then(() => {
|
||||||
.then(() => {
|
const finalUser = {
|
||||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
id: user.id,
|
||||||
session.user = finalUser
|
username: user.username,
|
||||||
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
role: user.role,
|
||||||
|
}
|
||||||
|
session.user = finalUser
|
||||||
|
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
||||||
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -170,5 +183,5 @@ module.exports = {
|
||||||
generateAttestationOptions,
|
generateAttestationOptions,
|
||||||
generateAssertionOptions,
|
generateAssertionOptions,
|
||||||
validateAttestation,
|
validateAttestation,
|
||||||
validateAssertion
|
validateAssertion,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,19 @@ const generateAttestationOptions = (session, options) => {
|
||||||
excludeCredentials: devices.map(dev => ({
|
excludeCredentials: devices.map(dev => ({
|
||||||
id: dev.data.credentialID,
|
id: dev.data.credentialID,
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
})),
|
})),
|
||||||
authenticatorSelection: {
|
authenticatorSelection: {
|
||||||
authenticatorAttachment: 'cross-platform',
|
authenticatorAttachment: 'cross-platform',
|
||||||
userVerification: 'discouraged',
|
userVerification: 'discouraged',
|
||||||
requireResidentKey: false
|
requireResidentKey: false,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
session.webauthn = {
|
session.webauthn = {
|
||||||
attestation: {
|
attestation: {
|
||||||
challenge: opts.challenge
|
challenge: opts.challenge,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
|
|
@ -48,16 +48,16 @@ const generateAssertionOptions = (session, options) => {
|
||||||
allowCredentials: devices.map(dev => ({
|
allowCredentials: devices.map(dev => ({
|
||||||
id: dev.data.credentialID,
|
id: dev.data.credentialID,
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
transports: ['usb', 'ble', 'nfc', 'internal'],
|
||||||
})),
|
})),
|
||||||
userVerification: 'discouraged',
|
userVerification: 'discouraged',
|
||||||
rpID: options.domain
|
rpID: options.domain,
|
||||||
})
|
})
|
||||||
|
|
||||||
session.webauthn = {
|
session.webauthn = {
|
||||||
assertion: {
|
assertion: {
|
||||||
challenge: opts.challenge
|
challenge: opts.challenge,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
return opts
|
return opts
|
||||||
})
|
})
|
||||||
|
|
@ -73,50 +73,52 @@ const validateAttestation = (session, options) => {
|
||||||
credential: options.attestationResponse,
|
credential: options.attestationResponse,
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain
|
expectedRPID: options.domain,
|
||||||
})
|
}),
|
||||||
])
|
]).then(([user, verification]) => {
|
||||||
.then(([user, verification]) => {
|
const { verified, attestationInfo } = verification
|
||||||
const { verified, attestationInfo } = verification
|
|
||||||
|
if (!(verified || attestationInfo)) {
|
||||||
|
session.webauthn = null
|
||||||
|
return verified
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
fmt,
|
||||||
|
counter,
|
||||||
|
aaguid,
|
||||||
|
credentialPublicKey,
|
||||||
|
credentialID,
|
||||||
|
credentialType,
|
||||||
|
userVerified,
|
||||||
|
attestationObject,
|
||||||
|
} = attestationInfo
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
.getHardwareCredentialsByUserId(user.id)
|
||||||
|
.then(userDevices => {
|
||||||
|
const existingDevice = userDevices.find(
|
||||||
|
device => device.data.credentialID === credentialID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!existingDevice) {
|
||||||
|
const newDevice = {
|
||||||
|
fmt,
|
||||||
|
counter,
|
||||||
|
aaguid,
|
||||||
|
credentialPublicKey,
|
||||||
|
credentialID,
|
||||||
|
credentialType,
|
||||||
|
userVerified,
|
||||||
|
attestationObject,
|
||||||
|
}
|
||||||
|
credentials.createHardwareCredential(user.id, newDevice)
|
||||||
|
}
|
||||||
|
|
||||||
if (!(verified || attestationInfo)) {
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
}
|
})
|
||||||
|
})
|
||||||
const {
|
|
||||||
fmt,
|
|
||||||
counter,
|
|
||||||
aaguid,
|
|
||||||
credentialPublicKey,
|
|
||||||
credentialID,
|
|
||||||
credentialType,
|
|
||||||
userVerified,
|
|
||||||
attestationObject
|
|
||||||
} = attestationInfo
|
|
||||||
|
|
||||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
|
||||||
.then(userDevices => {
|
|
||||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
|
||||||
|
|
||||||
if (!existingDevice) {
|
|
||||||
const newDevice = {
|
|
||||||
fmt,
|
|
||||||
counter,
|
|
||||||
aaguid,
|
|
||||||
credentialPublicKey,
|
|
||||||
credentialID,
|
|
||||||
credentialType,
|
|
||||||
userVerified,
|
|
||||||
attestationObject
|
|
||||||
}
|
|
||||||
credentials.createHardwareCredential(user.id, newDevice)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.webauthn = null
|
|
||||||
return verified
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAssertion = (session, options) => {
|
const validateAssertion = (session, options) => {
|
||||||
|
|
@ -124,17 +126,24 @@ const validateAssertion = (session, options) => {
|
||||||
|
|
||||||
return credentials.getHardwareCredentials().then(devices => {
|
return credentials.getHardwareCredentials().then(devices => {
|
||||||
const dbAuthenticator = _.find(dev => {
|
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)
|
}, devices)
|
||||||
|
|
||||||
if (!dbAuthenticator.data) {
|
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(
|
const convertedAuthenticator = _.merge(dbAuthenticator.data, {
|
||||||
dbAuthenticator.data,
|
credentialPublicKey: Buffer.from(
|
||||||
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
|
dbAuthenticator.data.credentialPublicKey,
|
||||||
)
|
),
|
||||||
|
})
|
||||||
|
|
||||||
let verification
|
let verification
|
||||||
try {
|
try {
|
||||||
|
|
@ -143,7 +152,7 @@ const validateAssertion = (session, options) => {
|
||||||
expectedChallenge: `${expectedChallenge}`,
|
expectedChallenge: `${expectedChallenge}`,
|
||||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||||
expectedRPID: options.domain,
|
expectedRPID: options.domain,
|
||||||
authenticator: convertedAuthenticator
|
authenticator: convertedAuthenticator,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
@ -160,16 +169,19 @@ const validateAssertion = (session, options) => {
|
||||||
dbAuthenticator.data.counter = assertionInfo.newCounter
|
dbAuthenticator.data.counter = assertionInfo.newCounter
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
credentials.updateHardwareCredential(dbAuthenticator),
|
credentials.updateHardwareCredential(dbAuthenticator),
|
||||||
users.getUserById(dbAuthenticator.user_id)
|
users.getUserById(dbAuthenticator.user_id),
|
||||||
])
|
]).then(([, user]) => {
|
||||||
.then(([_, user]) => {
|
const finalUser = {
|
||||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
id: user.id,
|
||||||
session.user = finalUser
|
username: user.username,
|
||||||
session.cookie.maxAge = REMEMBER_ME_AGE
|
role: user.role,
|
||||||
|
}
|
||||||
|
session.user = finalUser
|
||||||
|
session.cookie.maxAge = REMEMBER_ME_AGE
|
||||||
|
|
||||||
session.webauthn = null
|
session.webauthn = null
|
||||||
return verified
|
return verified
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,5 +189,5 @@ module.exports = {
|
||||||
generateAttestationOptions,
|
generateAttestationOptions,
|
||||||
generateAssertionOptions,
|
generateAssertionOptions,
|
||||||
validateAttestation,
|
validateAttestation,
|
||||||
validateAssertion
|
validateAssertion,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const FIDOUsernameless = require('./FIDOUsernamelessStrategy')
|
||||||
const STRATEGIES = {
|
const STRATEGIES = {
|
||||||
FIDO2FA,
|
FIDO2FA,
|
||||||
FIDOPasswordless,
|
FIDOPasswordless,
|
||||||
FIDOUsernameless
|
FIDOUsernameless,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
|
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
|
||||||
|
|
@ -13,5 +13,5 @@ const CHOSEN_STRATEGY = 'FIDO2FA'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CHOSEN_STRATEGY,
|
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 REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
||||||
const authenticateUser = (username, password) => {
|
const authenticateUser = (username, password) => {
|
||||||
return users.getUserByUsername(username)
|
return users
|
||||||
|
.getUserByUsername(username)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
const hashedPassword = user.password
|
const hashedPassword = user.password
|
||||||
if (!hashedPassword || !user.enabled) throw new authErrors.InvalidCredentialsError()
|
if (!hashedPassword || !user.enabled)
|
||||||
return Promise.all([argon2.verify(hashedPassword, password), hashedPassword])
|
throw new authErrors.InvalidCredentialsError()
|
||||||
|
return Promise.all([
|
||||||
|
argon2.verify(hashedPassword, password),
|
||||||
|
hashedPassword,
|
||||||
|
])
|
||||||
})
|
})
|
||||||
.then(([isMatch, hashedPassword]) => {
|
.then(([isMatch, hashedPassword]) => {
|
||||||
if (!isMatch) throw new authErrors.InvalidCredentialsError()
|
if (!isMatch) throw new authErrors.InvalidCredentialsError()
|
||||||
|
|
@ -32,7 +37,9 @@ const authenticateUser = (username, password) => {
|
||||||
|
|
||||||
const destroySessionIfSameUser = (context, user) => {
|
const destroySessionIfSameUser = (context, user) => {
|
||||||
const sessionUser = getUserFromCookie(context)
|
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) => {
|
const destroySessionIfBeingUsed = (sessID, context) => {
|
||||||
|
|
@ -56,15 +63,13 @@ const initializeSession = (context, user, rememberMe) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeProtectedAction = (code, id, context, action) => {
|
const executeProtectedAction = (code, id, context, action) => {
|
||||||
return users.getUserById(id)
|
return users.getUserById(id).then(user => {
|
||||||
.then(user => {
|
if (user.role !== 'superuser') {
|
||||||
if (user.role !== 'superuser') {
|
return action()
|
||||||
return action()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return confirm2FA(code, context)
|
return confirm2FA(code, context).then(() => action())
|
||||||
.then(() => action())
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserData = context => {
|
const getUserData = context => {
|
||||||
|
|
@ -79,10 +84,18 @@ const get2FASecret = (username, password) => {
|
||||||
return authenticateUser(username, password)
|
return authenticateUser(username, password)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
const secret = otplib.authenticator.generateSecret()
|
const secret = otplib.authenticator.generateSecret()
|
||||||
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
|
const otpauth = otplib.authenticator.keyuri(
|
||||||
return Promise.all([users.saveTemp2FASecret(user.id, secret), secret, otpauth])
|
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 }
|
return { secret, otpauth }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -103,35 +116,43 @@ const confirm2FA = (token, context) => {
|
||||||
|
|
||||||
const validateRegisterLink = token => {
|
const validateRegisterLink = token => {
|
||||||
if (!token) throw new authErrors.InvalidUrlError()
|
if (!token) throw new authErrors.InvalidUrlError()
|
||||||
return users.validateUserRegistrationToken(token)
|
return users.validateUserRegistrationToken(token).then(r => {
|
||||||
.then(r => {
|
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
return { username: r.username, role: r.role }
|
||||||
return { username: r.username, role: r.role }
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateResetPasswordLink = token => {
|
const validateResetPasswordLink = token => {
|
||||||
if (!token) throw new authErrors.InvalidUrlError()
|
if (!token) throw new authErrors.InvalidUrlError()
|
||||||
return users.validateAuthToken(token, 'reset_password')
|
return users.validateAuthToken(token, 'reset_password').then(r => {
|
||||||
.then(r => {
|
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
return { id: r.userID }
|
||||||
return { id: r.userID }
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateReset2FALink = token => {
|
const validateReset2FALink = token => {
|
||||||
if (!token) throw new authErrors.InvalidUrlError()
|
if (!token) throw new authErrors.InvalidUrlError()
|
||||||
return users.validateAuthToken(token, 'reset_twofa')
|
return users
|
||||||
|
.validateAuthToken(token, 'reset_twofa')
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||||
return users.getUserById(r.userID)
|
return users.getUserById(r.userID)
|
||||||
})
|
})
|
||||||
.then(user => {
|
.then(user => {
|
||||||
const secret = otplib.authenticator.generateSecret()
|
const secret = otplib.authenticator.generateSecret()
|
||||||
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
|
const otpauth = otplib.authenticator.keyuri(
|
||||||
return Promise.all([users.saveTemp2FASecret(user.id, secret), user, secret, otpauth])
|
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 }
|
return { user_id: user.id, secret, otpauth }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +165,10 @@ const deleteSession = (sessionID, context) => {
|
||||||
const login = (username, password) => {
|
const login = (username, password) => {
|
||||||
return authenticateUser(username, password)
|
return authenticateUser(username, password)
|
||||||
.then(user => {
|
.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]) => {
|
.then(([devices, twoFASecret]) => {
|
||||||
if (!_.isEmpty(devices)) return 'FIDO'
|
if (!_.isEmpty(devices)) return 'FIDO'
|
||||||
|
|
@ -153,21 +177,32 @@ const login = (username, password) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const input2FA = (username, password, rememberMe, code, context) => {
|
const input2FA = (username, password, rememberMe, code, context) => {
|
||||||
return authenticateUser(username, password)
|
return authenticateUser(username, password).then(user => {
|
||||||
.then(user => {
|
const secret = user.twofa_code
|
||||||
const secret = user.twofa_code
|
const isCodeValid = otplib.authenticator.verify({
|
||||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
token: code,
|
||||||
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
secret: secret,
|
||||||
|
|
||||||
initializeSession(context, user, rememberMe)
|
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
|
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||||
|
|
||||||
|
initializeSession(context, user, rememberMe)
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup2FA = (username, password, rememberMe, codeConfirmation, context) => {
|
const setup2FA = (
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
rememberMe,
|
||||||
|
codeConfirmation,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
return authenticateUser(username, password)
|
return authenticateUser(username, password)
|
||||||
.then(user => {
|
.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()
|
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||||
|
|
||||||
initializeSession(context, user, rememberMe)
|
initializeSession(context, user, rememberMe)
|
||||||
|
|
@ -202,24 +237,23 @@ const createReset2FAToken = (code, userID, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRegisterToken = (username, role) => {
|
const createRegisterToken = (username, role) => {
|
||||||
return users.getUserByUsername(username)
|
return users.getUserByUsername(username).then(user => {
|
||||||
.then(user => {
|
if (user) throw new authErrors.UserAlreadyExistsError()
|
||||||
if (user) throw new authErrors.UserAlreadyExistsError()
|
|
||||||
|
|
||||||
return users.createUserRegistrationToken(username, role)
|
return users.createUserRegistrationToken(username, role)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = (token, username, password, role) => {
|
const register = (token, username, password, role) => {
|
||||||
return users.getUserByUsername(username)
|
return users.getUserByUsername(username).then(user => {
|
||||||
.then(user => {
|
if (user) throw new authErrors.UserAlreadyExistsError()
|
||||||
if (user) throw new authErrors.UserAlreadyExistsError()
|
return users.register(token, username, password, role).then(() => true)
|
||||||
return users.register(token, username, password, role).then(() => true)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetPassword = (token, userID, newPassword, context) => {
|
const resetPassword = (token, userID, newPassword, context) => {
|
||||||
return users.getUserById(userID)
|
return users
|
||||||
|
.getUserById(userID)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
destroySessionIfSameUser(context, user)
|
destroySessionIfSameUser(context, user)
|
||||||
return users.updatePassword(token, user.id, newPassword)
|
return users.updatePassword(token, user.id, newPassword)
|
||||||
|
|
@ -228,9 +262,13 @@ const resetPassword = (token, userID, newPassword, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset2FA = (token, userID, code, context) => {
|
const reset2FA = (token, userID, code, context) => {
|
||||||
return users.getUserById(userID)
|
return users
|
||||||
|
.getUserById(userID)
|
||||||
.then(user => {
|
.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()
|
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||||
|
|
||||||
destroySessionIfSameUser(context, user)
|
destroySessionIfSameUser(context, user)
|
||||||
|
|
@ -240,7 +278,10 @@ const reset2FA = (token, userID, code, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getToken = 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')
|
throw new authErrors.AuthenticationError('Authentication failed')
|
||||||
|
|
||||||
return context.req.session.user.id
|
return context.req.session.user.id
|
||||||
|
|
@ -267,5 +308,5 @@ module.exports = {
|
||||||
register,
|
register,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
reset2FA,
|
reset2FA,
|
||||||
getToken
|
getToken,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ const bills = require('../../services/bills')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
bills: (...[, { filters }]) => bills.getBills(filters)
|
bills: (...[, { filters }]) => bills.getBills(filters),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const blacklist = require('../../../blacklist')
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
blacklist: () => blacklist.getBlacklist(),
|
blacklist: () => blacklist.getBlacklist(),
|
||||||
blacklistMessages: () => blacklist.getMessages()
|
blacklistMessages: () => blacklist.getMessages(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
deleteBlacklistRow: (...[, { address }]) =>
|
deleteBlacklistRow: (...[, { address }]) =>
|
||||||
|
|
@ -11,8 +11,8 @@ const resolvers = {
|
||||||
insertBlacklistRow: (...[, { address }]) =>
|
insertBlacklistRow: (...[, { address }]) =>
|
||||||
blacklist.insertIntoBlacklist(address),
|
blacklist.insertIntoBlacklist(address),
|
||||||
editBlacklistMessage: (...[, { id, content }]) =>
|
editBlacklistMessage: (...[, { id, content }]) =>
|
||||||
blacklist.editBlacklistMessage(id, content)
|
blacklist.editBlacklistMessage(id, content),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,21 @@ const logDateFormat = require('../../../logs').logDateFormat
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
cashboxBatches: () => cashbox.getBatches(),
|
cashboxBatches: () => cashbox.getBatches(),
|
||||||
cashboxBatchesCsv: (...[, { from, until, timezone }]) => cashbox.getBatches(from, until)
|
cashboxBatchesCsv: (...[, { from, until, timezone }]) =>
|
||||||
.then(data => parseAsync(logDateFormat(timezone, cashbox.logFormatter(data), ['created'])))
|
cashbox
|
||||||
|
.getBatches(from, until)
|
||||||
|
.then(data =>
|
||||||
|
parseAsync(
|
||||||
|
logDateFormat(timezone, cashbox.logFormatter(data), ['created']),
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount),
|
createBatch: (...[, { deviceId, cashboxCount }]) =>
|
||||||
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
|
cashbox.createCashboxBatch(deviceId, cashboxCount),
|
||||||
}
|
editBatch: (...[, { id, performedBy }]) =>
|
||||||
|
cashbox.editBatchById(id, performedBy),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
const { accounts: accountsConfig, countries, languages } = require('../../config')
|
const {
|
||||||
|
accounts: accountsConfig,
|
||||||
|
countries,
|
||||||
|
languages,
|
||||||
|
} = require('../../config')
|
||||||
|
|
||||||
const resolver = {
|
const resolver = {
|
||||||
Query: {
|
Query: {
|
||||||
countries: () => countries,
|
countries: () => countries,
|
||||||
languages: () => languages,
|
languages: () => languages,
|
||||||
accountsConfig: () => accountsConfig
|
accountsConfig: () => accountsConfig,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolver
|
module.exports = resolver
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ const { coins, currencies } = require('../../config')
|
||||||
const resolver = {
|
const resolver = {
|
||||||
Query: {
|
Query: {
|
||||||
currencies: () => currencies,
|
currencies: () => currencies,
|
||||||
cryptoCurrencies: () => coins
|
cryptoCurrencies: () => coins,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolver
|
module.exports = resolver
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,55 @@ const authentication = require('../modules/userManagement')
|
||||||
const queries = require('../../services/customInfoRequests')
|
const queries = require('../../services/customInfoRequests')
|
||||||
const DataLoader = require('dataloader')
|
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 = {
|
const resolvers = {
|
||||||
Customer: {
|
Customer: {
|
||||||
customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id)
|
customInfoRequests: parent =>
|
||||||
|
customerCustomInfoRequestsLoader.load(parent.id),
|
||||||
},
|
},
|
||||||
CustomRequestData: {
|
CustomRequestData: {
|
||||||
customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId)
|
customInfoRequest: parent =>
|
||||||
|
customInfoRequestLoader.load(parent.infoRequestId),
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled),
|
customInfoRequests: (...[, { onlyEnabled }]) =>
|
||||||
customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId),
|
queries.getCustomInfoRequests(onlyEnabled),
|
||||||
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId)
|
customerCustomInfoRequests: (...[, { customerId }]) =>
|
||||||
|
queries.getAllCustomInfoRequestsForCustomer(customerId),
|
||||||
|
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) =>
|
||||||
|
queries.getCustomInfoRequestForCustomer(customerId, infoRequestId),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
|
insertCustomInfoRequest: (...[, { customRequest }]) =>
|
||||||
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
|
queries.addCustomInfoRequest(customRequest),
|
||||||
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
|
removeCustomInfoRequest: (...[, { id }]) =>
|
||||||
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => {
|
queries.removeCustomInfoRequest(id),
|
||||||
|
editCustomInfoRequest: (...[, { id, customRequest }]) =>
|
||||||
|
queries.editCustomInfoRequest(id, customRequest),
|
||||||
|
setAuthorizedCustomRequest: (
|
||||||
|
...[, { customerId, infoRequestId, override }, context]
|
||||||
|
) => {
|
||||||
const token = authentication.getToken(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
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -6,41 +6,56 @@ const customerNotes = require('../../../customer-notes')
|
||||||
const machineLoader = require('../../../machine-loader')
|
const machineLoader = require('../../../machine-loader')
|
||||||
|
|
||||||
const addLastUsedMachineName = customer =>
|
const addLastUsedMachineName = customer =>
|
||||||
(customer.lastUsedMachine ? machineLoader.getMachineName(customer.lastUsedMachine) : Promise.resolve(null))
|
(customer.lastUsedMachine
|
||||||
.then(lastUsedMachineName => Object.assign(customer, { lastUsedMachineName }))
|
? machineLoader.getMachineName(customer.lastUsedMachine)
|
||||||
|
: Promise.resolve(null)
|
||||||
|
).then(lastUsedMachineName =>
|
||||||
|
Object.assign(customer, { lastUsedMachineName }),
|
||||||
|
)
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Customer: {
|
Customer: {
|
||||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
isAnonymous: parent => parent.customerId === anonymous.uuid,
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
customers: (...[, { phone, email, name, address, id }]) => customers.getCustomersList(phone, name, address, id, email),
|
customers: (...[, { phone, email, name, address, id }]) =>
|
||||||
customer: (...[, { customerId }]) => customers.getCustomerById(customerId).then(addLastUsedMachineName),
|
customers.getCustomersList(phone, name, address, id, email),
|
||||||
customerFilters: () => filters.customer()
|
customer: (...[, { customerId }]) =>
|
||||||
|
customers.getCustomerById(customerId).then(addLastUsedMachineName),
|
||||||
|
customerFilters: () => filters.customer(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
setCustomer: (root, { customerId, customerInput }, context, info) => {
|
setCustomer: (root, { customerId, customerInput }, context) => {
|
||||||
const token = authentication.getToken(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)
|
return customers.updateCustomer(customerId, customerInput, token)
|
||||||
},
|
},
|
||||||
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
|
addCustomField: (...[, { customerId, label, value }]) =>
|
||||||
saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value),
|
customers.addCustomField(customerId, label, value),
|
||||||
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
|
saveCustomField: (...[, { customerId, fieldId, value }]) =>
|
||||||
|
customers.saveCustomField(customerId, fieldId, value),
|
||||||
|
removeCustomField: (...[, [{ customerId, fieldId }]]) =>
|
||||||
|
customers.removeCustomField(customerId, fieldId),
|
||||||
editCustomer: async (root, { customerId, customerEdit }, context) => {
|
editCustomer: async (root, { customerId, customerEdit }, context) => {
|
||||||
const token = authentication.getToken(context)
|
const token = authentication.getToken(context)
|
||||||
const editedData = await customerEdit
|
const editedData = await customerEdit
|
||||||
return customers.edit(customerId, editedData, token)
|
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 token = authentication.getToken(context)
|
||||||
const { file } = newPhoto
|
const { file } = newPhoto
|
||||||
const photo = await file
|
const photo = await file
|
||||||
if (!photo) return customers.getCustomerById(customerId)
|
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))
|
.then(newPatch => customers.edit(customerId, newPatch, token))
|
||||||
},
|
},
|
||||||
deleteEditedData: (root, { customerId, customerEdit }) => {
|
deleteEditedData: (root, { customerId }) => {
|
||||||
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
|
||||||
return customers.getCustomerById(customerId)
|
return customers.getCustomerById(customerId)
|
||||||
},
|
},
|
||||||
|
|
@ -55,12 +70,13 @@ const resolvers = {
|
||||||
deleteCustomerNote: (...[, { noteId }]) => {
|
deleteCustomerNote: (...[, { noteId }]) => {
|
||||||
return customerNotes.deleteCustomerNote(noteId)
|
return customerNotes.deleteCustomerNote(noteId)
|
||||||
},
|
},
|
||||||
createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }),
|
createCustomer: (...[, { phoneNumber }]) =>
|
||||||
|
customers.add({ phone: phoneNumber }),
|
||||||
enableTestCustomer: (...[, { customerId }]) =>
|
enableTestCustomer: (...[, { customerId }]) =>
|
||||||
customers.enableTestCustomer(customerId),
|
customers.enableTestCustomer(customerId),
|
||||||
disableTestCustomer: (...[, { customerId }]) =>
|
disableTestCustomer: (...[, { customerId }]) =>
|
||||||
customers.disableTestCustomer(customerId)
|
customers.disableTestCustomer(customerId),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ const funding = require('../../services/funding')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
funding: () => funding.getFunding()
|
funding: () => funding.getFunding(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const resolvers = [
|
||||||
status,
|
status,
|
||||||
transaction,
|
transaction,
|
||||||
user,
|
user,
|
||||||
version
|
version,
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = mergeResolvers(resolvers)
|
module.exports = mergeResolvers(resolvers)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
const { parseAsync } = require('json2csv')
|
const { parseAsync } = require('json2csv')
|
||||||
const _ = require('lodash/fp')
|
|
||||||
|
|
||||||
const logs = require('../../../logs')
|
const logs = require('../../../logs')
|
||||||
const serverLogs = require('../../services/server-logs')
|
const serverLogs = require('../../services/server-logs')
|
||||||
|
|
@ -8,15 +7,23 @@ const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
|
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
|
||||||
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
|
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
|
||||||
machineLogsCsv: (...[, { deviceId, from, until, limit, offset, timezone }]) =>
|
machineLogsCsv: (
|
||||||
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset)
|
...[, { deviceId, from, until, limit, offset, timezone }]
|
||||||
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp']))),
|
) =>
|
||||||
|
logs
|
||||||
|
.simpleGetMachineLogs(deviceId, from, until, limit, offset)
|
||||||
|
.then(res =>
|
||||||
|
parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])),
|
||||||
|
),
|
||||||
serverLogs: (...[, { from, until, limit, offset }]) =>
|
serverLogs: (...[, { from, until, limit, offset }]) =>
|
||||||
serverLogs.getServerLogs(from, until, limit, offset),
|
serverLogs.getServerLogs(from, until, limit, offset),
|
||||||
serverLogsCsv: (...[, { from, until, limit, offset, timezone }]) =>
|
serverLogsCsv: (...[, { from, until, limit, offset, timezone }]) =>
|
||||||
serverLogs.getServerLogs(from, until, limit, offset)
|
serverLogs
|
||||||
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])))
|
.getServerLogs(from, until, limit, offset)
|
||||||
}
|
.then(res =>
|
||||||
|
parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])),
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,30 @@ const DataLoader = require('dataloader')
|
||||||
const loyalty = require('../../../loyalty')
|
const loyalty = require('../../../loyalty')
|
||||||
const { getSlimCustomerByIdBatch } = require('../../../customers')
|
const { getSlimCustomerByIdBatch } = require('../../../customers')
|
||||||
|
|
||||||
const customerLoader = new DataLoader(ids => {
|
const customerLoader = new DataLoader(
|
||||||
return getSlimCustomerByIdBatch(ids)
|
ids => {
|
||||||
}, { cache: false })
|
return getSlimCustomerByIdBatch(ids)
|
||||||
|
},
|
||||||
|
{ cache: false },
|
||||||
|
)
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
IndividualDiscount: {
|
IndividualDiscount: {
|
||||||
customer: parent => customerLoader.load(parent.customerId)
|
customer: parent => customerLoader.load(parent.customerId),
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
promoCodes: () => loyalty.getAvailablePromoCodes(),
|
promoCodes: () => loyalty.getAvailablePromoCodes(),
|
||||||
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts()
|
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
|
createPromoCode: (...[, { code, discount }]) =>
|
||||||
|
loyalty.createPromoCode(code, discount),
|
||||||
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
|
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
|
||||||
createIndividualDiscount: (...[, { customerId, discount }]) => loyalty.createIndividualDiscount(customerId, discount),
|
createIndividualDiscount: (...[, { customerId, discount }]) =>
|
||||||
deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId)
|
loyalty.createIndividualDiscount(customerId, discount),
|
||||||
}
|
deleteIndividualDiscount: (...[, { discountId }]) =>
|
||||||
|
loyalty.deleteIndividualDiscount(discountId),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,29 @@ const DataLoader = require('dataloader')
|
||||||
const { machineAction } = require('../../services/machines')
|
const { machineAction } = require('../../services/machines')
|
||||||
|
|
||||||
const machineLoader = require('../../../machine-loader')
|
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(
|
||||||
return machineEventsByIdBatch(ids)
|
ids => {
|
||||||
}, { cache: false })
|
return machineEventsByIdBatch(ids)
|
||||||
|
},
|
||||||
|
{ cache: false },
|
||||||
|
)
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Machine: {
|
Machine: {
|
||||||
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
|
latestEvent: parent => machineEventsLoader.load(parent.deviceId),
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
machines: () => machineLoader.getMachineNames(),
|
machines: () => machineLoader.getMachineNames(),
|
||||||
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
|
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
|
||||||
unpairedMachines: () => machineLoader.getUnpairedMachines()
|
unpairedMachines: () => machineLoader.getUnpairedMachines(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
|
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
|
||||||
machineAction({ deviceId, action, cashUnits, newName }, context)
|
machineAction({ deviceId, action, cashUnits, newName }, context),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ const exchange = require('../../../exchange')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
getMarkets: () => exchange.getMarkets()
|
getMarkets: () => exchange.getMarkets(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
notifications: () => notifierQueries.getNotifications(),
|
notifications: () => notifierQueries.getNotifications(),
|
||||||
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications(),
|
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications(),
|
||||||
alerts: () => notifierQueries.getAlerts()
|
alerts: () => notifierQueries.getAlerts(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read),
|
toggleClearNotification: (...[, { id, read }]) =>
|
||||||
clearAllNotifications: () => notifierQueries.markAllAsRead()
|
notifierQueries.setRead(id, read),
|
||||||
}
|
clearAllNotifications: () => notifierQueries.markAllAsRead(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ const pairing = require('../../services/pairing')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createPairingTotem: (...[, { name }]) => pairing.totem(name)
|
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ const resolvers = {
|
||||||
return pi.getRawRates().then(r => {
|
return pi.getRawRates().then(r => {
|
||||||
return {
|
return {
|
||||||
withCommissions: pi.buildRates(r),
|
withCommissions: pi.buildRates(r),
|
||||||
withoutCommissions: pi.buildRatesNoCommission(r)
|
withoutCommissions: pi.buildRatesNoCommission(r),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
fiatRates: () => forex.getFiatRates()
|
fiatRates: () => forex.getFiatRates(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ const resolvers = {
|
||||||
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
||||||
const token = authentication.getToken(context)
|
const token = authentication.getToken(context)
|
||||||
return sanctions.checkByUser(customerId, token)
|
return sanctions.checkByUser(customerId, token)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars')
|
const {
|
||||||
|
DateTimeISOResolver,
|
||||||
|
JSONResolver,
|
||||||
|
JSONObjectResolver,
|
||||||
|
} = require('graphql-scalars')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
JSON: JSONResolver,
|
JSON: JSONResolver,
|
||||||
JSONObject: JSONObjectResolver,
|
JSONObject: JSONObjectResolver,
|
||||||
DateTimeISO: DateTimeISOResolver
|
DateTimeISO: DateTimeISOResolver,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ const settingsLoader = require('../../../new-settings-loader')
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
accounts: () => settingsLoader.showAccounts(),
|
accounts: () => settingsLoader.showAccounts(),
|
||||||
config: () => settingsLoader.loadLatestConfigOrNone()
|
config: () => settingsLoader.loadLatestConfigOrNone(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
saveAccounts: (...[, { accounts }]) =>
|
||||||
|
settingsLoader.saveAccounts(accounts),
|
||||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config),
|
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ const smsNotices = require('../../../sms-notices')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
SMSNotices: () => smsNotices.getSMSNotices()
|
SMSNotices: () => smsNotices.getSMSNotices(),
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
|
editSMSNotice: (...[, { id, event, message }]) =>
|
||||||
|
smsNotices.editSMSNotice(id, event, message),
|
||||||
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
|
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
|
||||||
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
|
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ const supervisor = require('../../services/supervisor')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
uptime: () => supervisor.getAllProcessInfo()
|
uptime: () => supervisor.getAllProcessInfo(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = resolvers
|
module.exports = resolvers
|
||||||
|
|
|
||||||
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